Build an Internal Container Platform Without Kubernetes

Build an Internal Container Platform Without Kubernetes
Build an Internal Container Platform Without Kubernetes

An internal container platform should make the safe way to deploy software the easy way. That does not automatically require Kubernetes. A team running a modest set of APIs, workers, scheduled tasks and internal tools can standardize deployment, security and recovery around container engines and conventional Linux hosts. The critical distinction is between running containers and operating a service platform: the latter needs ownership of changes, traffic, data and failures.

The right design starts with the application's operational contract, not an orchestrator feature list. A dashboard that launches containers is useful, but it is not a complete platform. A cluster that restarts tasks cannot recover a corrupted database. Conversely, a single server with disciplined releases may serve an appropriately constrained workload without a distributed scheduler.

Specify the platform contract before choosing software

Define what every application team must provide: a buildable image, a listening port or background-worker command, readiness and liveness behavior, a configuration schema, resource expectations, an accountable owner and a rollback method. Then decide what the platform guarantees: an authenticated deployment path, stable DNS and TLS, isolation between applications, a durable audit trail, secret delivery, centralized telemetry, backed-up data and incident access.

Separate capability from mechanism. A service catalog could begin as a reviewed repository containing app manifests and ownership metadata. A deployment API could be a CI pipeline with tightly scoped credentials. An environment could be a directory of versioned configuration instead of an elaborate resource hierarchy. These are design choices, not claims that minimal tooling provides equivalent governance automatically.

Write explicit non-goals. If the platform initially supports only Linux container images, one region, stateless HTTP services and scheduled workers, say so. Refuse silent assumptions about automatic geographic failover, shared volumes or unlimited horizontal scaling.

Document the developer-facing contract

Provide a small manifest or template with fields for service owner, runtime port, health endpoint, environment, dependency names, expected concurrency, resource ceiling and persistent-data classification. Derive dashboards and deployment controls from it rather than asking developers to repeat metadata in unrelated systems. Keep environment differences in version-controlled configuration; reference secret names, never secret values. The contract should describe what happens during a failed readiness check, a stopped host and a bad deployment.

Avoid making the application template an unreviewable custom framework. Its value is that engineers can run it locally, inspect the resulting engine configuration, and predict how a deployment will be performed. Publish migration instructions when the contract changes.

Select the smallest viable runtime

For a single Linux host, Docker Compose is a legitimate production mechanism. Docker's production guide describes production-specific configuration overlays, restart policies and deployment to remote engines. Compose provides predictable declarations for related services. It does not, by itself, coordinate a replica set across independent hosts.

Podman plus Quadlet offers a different operational style. Quadlet converts declarative container files into systemd units, including configurations in rootless user paths. This fits teams already treating systemd, journal logs and OS-level service lifecycle as their common management layer. It is not a cross-host placement engine.

For application teams that want a higher-level interface, Dokku provides Git-push or image-building workflows primarily oriented around a server the operator controls. Coolify offers a dashboard and API for building, deploying and managing Docker resources over SSH-connected machines. Kamal uses image builds, a registry and an application proxy to perform server deployments. These products reduce deployment scripting, but they do not transfer infrastructure responsibility to a managed cloud provider.

A small selection guide helps:

Workload shape Reasonable starting point Key boundary
One host, several related services Compose Host remains a failure domain
One host, OS-native operations and rootless containers Podman Quadlet No built-in cluster placement
Git-push developer workflow Dokku Validate desired scheduler and host model
Self-service dashboard for Docker apps Coolify Multi-server traffic and storage need design
Image-based deployment to known servers Kamal Server placement and shared data remain explicit
Dynamic container placement across hosts Docker Swarm or Nomad Requires cluster administration

These are architectural fits, not a ranking of reliability, performance or total cost.

Build a reproducible deployment path

The unit of release should be an immutable container image referenced by a precise digest, accompanied by deployment configuration and a rollback target. CI should run tests, build the image once, record its provenance and publish it to a registry. Promotion to staging and production should reuse the same artifact instead of rebuilding from mutable source at each stage.

A basic application definition might look like this:

services:
  api:
    image: registry.example.test/team/api@sha256:<verified-digest>
    restart: unless-stopped
    read_only: true
    ports:
      - "127.0.0.1:8080:8080"
    healthcheck:
      test: ["CMD", "/app/check-ready"]
      interval: 30s
      timeout: 5s
      retries: 3

The digest is deliberately a placeholder, not a runnable image reference. Treat the exact image and its configuration as a single approved release. The service listens only on loopback in this illustration; an independently managed reverse proxy would forward authenticated public traffic where required.

Docker documents health-aware Compose dependencies, including service_healthy. That controls dependency startup ordering; it does not prove a service will stay healthy, nor does it replace alerting, restart policy or rollback. Define readiness as the ability to serve an actual request, not simply a running process.

For web applications, use a reverse proxy with certificate automation, request limits, access logs and a stable upstream target. Deploy the new container, wait for readiness and only then switch traffic. Dokku's deployment checks and Kamal's proxy-assisted rollout are documented examples of the pattern. Their availability claims depend on application behavior, infrastructure capacity and correct health checks.

An opinionated first-host implementation

Use a hardened Linux machine with centrally managed SSH keys, security updates and time synchronization. Install the chosen engine from a supported distribution channel. Put the public reverse proxy on ports 80 and 443, keep application listeners on private or loopback interfaces, and close unnecessary inbound ports. Use a dedicated directory for deployment manifests, but do not allow applications to edit their own host configuration.

A simple CI release sequence is: run tests; build and scan the image; push it to a registry; verify its digest; acquire a per-environment deployment lock; render the reviewed manifest with that digest; pull and start the candidate container; check readiness from the proxy's network; switch traffic; record the release; and retain the prior digest for rollback. Each stage should have clear timeout and failure behavior. Never label a rollout successful just because the engine returned a container ID.

Define an operational break-glass path that does not depend on the same CI service used for routine deployment. Restrict it to approved operators and log its use. Store the process alongside the recovery documentation.

Keep deployment identity separate from runtime identity

CI should not need broad, long-lived root access to every host. Give deployment automation only the privileges its release path requires, and isolate build jobs from production credentials. Store environment-specific configuration away from images. Track who approved a production release and which digest was installed.

The container engine is a privileged control surface. Docker's daemon-access guidance explains SSH and mutual-TLS approaches and warns that access credentials can provide powerful control of the host. Do not publish an unauthenticated engine API or mount the Docker socket into arbitrary application containers.

Prefer non-root processes, minimally writable filesystems, limited Linux capabilities and explicit CPU and memory controls where supported. Scan dependencies and images as part of the build; maintain a patch path for base images. Pinning an image digest gives repeatability, not automatic safety. A pinned vulnerable image remains vulnerable until updated.

Secret handling differs across runtimes. Compose can expose file-mounted secrets to selected services, but those files still need secure provisioning on the host. Swarm's secret mechanism uses encrypted Raft storage and mounts authorized secrets into running Linux containers in memory. Neither excuses weak access controls, unrotated credentials or leaking secrets into logs.

Prevent a platform from becoming a shared-secret bundle

The simplest developer experience often tempts teams to give one CI token, one dashboard role and one machine account permission to change everything. That reduces setup friction but concentrates risk. Separate build, read-only observability, release approval and production deployment identities. Limit who can change ingress routes and persistent mounts. Record service ownership so incident responders know whom to involve.

Avoid accepting arbitrary deployment manifests from untrusted tenants on a shared Docker host without a robust isolation boundary. Rootless containers can reduce the impact of some privileges but do not replace workload isolation, kernel patching or restrictive network and data access.

Decide where application state lives

Treat application containers as replaceable. A database, uploaded files, queues with durability requirements and user sessions need an explicit persistence plan. Avoid writing irreplaceable data into the container's ephemeral filesystem.

For one host, a dedicated local volume may be appropriate, but it is not a disaster-recovery strategy. The Coolify storage documentation explicitly distinguishes persistent mounts from backups and notes that mounted storage is host-local. Define backup frequency from recovery-point objectives, test restoring to a clean machine, and record the credentials and dependencies needed to restore.

A platform should make the ownership of state visible. Either provide a separately operated data service with a documented backup contract or declare that each application owner must supply one. Do not promise transparent rescheduling of a stateful workload between hosts unless storage, fencing, consistency and recovery have actually been engineered.

Database changes complicate rollback. Prefer schema migrations that tolerate overlapping old and new application versions: add new fields before using them, backfill separately, shift reads and writes, and remove old fields only after deployment convergence. Application rollback cannot automatically reverse a destructive database migration.

Add a scheduler only when the requirements demand one

A multi-host cluster is justified when the platform must place workloads dynamically, replace tasks after host failures, schedule capacity across machines or coordinate replicas and updates. It is not justified simply because an organization has several servers.

Docker Swarm models services and desired-state tasks, with replicas, resource reservations, overlay networking and rolling-update policies. It is simpler to introduce into a Docker-oriented environment than a completely new container API, but it carries its own operating model. Docker's Swarm administration guide explains manager quorum: losing a majority can prevent management operations even while some existing workloads continue running. Back up cluster state and test recovery.

Beware a compatibility trap: docker compose up remains a single-engine workflow, even when that engine participates in Swarm. Docker notes that docker stack deploy uses a legacy Compose format, not the complete current Compose specification. Validate manifests against the actual deployment command instead of assuming local development and cluster production parse identical features.

Nomad is another workload scheduler that can place containers and other job types across hosts. HashiCorp's service discovery documentation describes its lighter native discovery and recommends Consul integration for more complex production service-discovery needs. Consul is a separately operated component; this can be an advantage for teams already using it, or additional overhead for smaller deployments.

Design traffic and recovery rather than assuming availability

Putting an application on multiple hosts is not sufficient for high availability. A load balancer must send traffic only to healthy origins. Sessions and background jobs must tolerate restarts and duplicate execution. Storage must survive the failure modes the platform claims to handle. Capacity must be sufficient after a host is lost.

Coolify's multi-server guidance makes the distinction explicit: extra application instances do not automatically create a traffic-distribution layer, and current support has restrictions for Compose applications and persistent storage. That is a documented product boundary, not a reason to dismiss its useful single-host capabilities.

Write recovery procedures for at least a failed application release, unhealthy reverse proxy, lost host, unavailable registry, expired certificate, leaked credential and corrupted database. Exercise them. A backup whose restore path is unknown is an assumption, not a control.

Define failure domains before increasing replica count

Map each service to its true dependency graph: reverse proxy, DNS, certificate provisioning, registry, identity provider, database, queue and outbound integrations. Two application replicas on separate hosts still fail together if both depend on a single database or a single upstream network path. If the registry is unavailable during recovery, nodes also need a defined strategy for obtaining the previously approved image.

Use a tabletop exercise with concrete scenarios: one host fails; the proxy can no longer reach a healthy origin; a credential is revoked; a schema migration breaks compatibility. Write what must remain available, what can be degraded, and what requires operator intervention. Let these tests drive the architecture rather than purchasing availability features without validating them.

Make operations visible from day one

Expose structured logs, basic request metrics, latency, error rates, CPU and memory pressure, disk capacity, container restart counts, certificate expiry and successful backups. Prefer a common telemetry schema with service name, environment, release digest and request correlation IDs. For traces, an OpenTelemetry Collector gateway is one documented pattern for routing signals to backends without coupling every service directly to a vendor endpoint.

Alert on conditions requiring action and document the expected responder. The objective is not to maximize dashboards. It is to answer three questions during a failure: what changed, which users are affected and how to restore service safely.

Measure operational cost as engineering time as well as infrastructure expense. Count platform upgrades, release failures, manual interventions, incident duration, credential rotation and onboarding friction. A nominally lightweight runtime can become costly if every service invents its own deployment conventions.

A staged rollout for a small engineering organization

Consider a hypothetical team operating a public API, a background worker, a scheduled importer and a small internal dashboard. Initially, a container registry, a single hardened Linux host, Compose or Quadlet, a reverse proxy, automated deployment and externalized database backups may provide a workable platform. It should use an explicit maintenance window or documented availability limits if host failure cannot be tolerated.

The first milestone is not a portal. It is one service deployed repeatedly from a tested pipeline, with a known rollback and a verified data restore. Next add templates and service ownership metadata, then a release approval process and observable health checks. After operating multiple services, inspect the deployment support burden before adding a dashboard or scheduler.

If the team later needs host-loss continuity, test the failure scenario first. It may choose a second instance behind a load balancer with external state, or adopt Swarm or Nomad for placement. Neither path is free: the first requires careful host coordination; the latter introduces cluster lifecycle work.

Establish exit criteria for the simple design

Before the first production release, require an identified service owner, a reviewed deployment definition, verified least-privilege credential delivery, resource limits, a documented restart procedure, a health check that catches real failure, a tested rollback, an actual restore test and access to logs during an incident. For a customer-facing service, also test TLS renewal and inbound traffic behavior under a stopped backend.

Revisit the architecture when host failures cause unacceptable downtime, placement and resource contention repeatedly need manual intervention, security boundaries become too broad, or platform changes consume more engineer time than application delivery. These are decision signals, not numerical thresholds. Document the observed failure and the requirement that the next architecture must satisfy.

Recognize the boundary of the approach

A self-hosted container platform is viable when its stated service-level requirements fit its hosting and staffing model. It becomes a poor fit when the organization needs capabilities it does not have time or authority to build: strong multi-tenant isolation, complex multi-region routing, dynamically changing scheduling constraints, managed infrastructure recovery or policy enforcement across many teams.

Kubernetes can be the right answer in those situations, but choosing it is still an engineering trade-off rather than a badge of maturity. The useful question is which operational guarantees are required, who will implement them, and how their correctness will be tested.

The durable outcome is a boring, auditable path from commit to running service, with ownership of data, credentials and recovery made explicit. Everything else is an implementation choice.