Skip to content

Blog

A deployment platform should not assume every app runs on port 3000

How Appaloft examples use explicit deployment contracts for Vite, Python, Go, and Docker Compose instead of making the platform guess.

AppaloftDeployment controlRuntimeDockerExamples

A minimal deployment example often looks like this: a Node server on port 3000, a /health endpoint, and a Dockerfile next to it.

That is useful for testing the shortest path. It can also give a deployment platform the wrong view of the world. Real repositories include Vite sites built and served by nginx, Python services on port 8000, Go programs compiled into a single binary, and Docker Compose stacks with a public web service and a private API. A Node hello world will never exercise many of those boundaries.

We recently expanded the public appaloft/examples repository. The point is not to teach five languages. It is to give the deployment control plane a set of differently shaped applications that can be tested locally and deployed from Git.

Read a contract instead of guessing the language

Appaloft does not need to see package.json and guess Node. It should not find main.go and choose a build process on the user’s behalf, either. Filenames are clues, not intent.

Each Git deployment example includes an appaloft.yml. The Vite example has a small contract:

name: appaloft-example-vite-static
source:
  path: .
runtime:
  method: docker-image
network:
  internalPort: 8080
access:
  default: public
health:
  path: /health

The control plane does not have to understand Vite. The Dockerfile runs npm run build and hands the output to nginx. The deployment contract says how to build the runtime, where traffic should enter, and which path reports health.

The Python example changes internalPort to 8000; the Go example uses 8080. Those unremarkable numbers catch a common bug: deployment code, proxy configuration, or a smoke script that quietly treats 3000 as a constant.

A repository is not a build context

Language is not the only variable. A monorepo also requires the deployment system to respect the base directory.

The public examples repository keeps each application in its own directory. Deploying vite-static uses the same Git source as the Go service, but its base directory is /vite-static; the Go service uses /go-http. The build context, Dockerfile, and configuration must all resolve from that boundary. They should not accidentally discover files from another application at the repository root.

This is why we keep hello/ as the minimal official launch smoke while adding broader catalog examples. The stable short path continuously checks the product entry point. The wider catalog covers different ports, builds, languages, and topologies. Making every variation part of the default smoke would make failures slower and harder to isolate.

Compose changes the topology

compose-stack is not another language sample. It changes the shape of the deployment.

It contains two services: a public web:8080 and a private api:3000. Its contract selects runtime.strategy: docker-compose and points to docker-compose.yml. The web service reaches the API over the Compose network; only the web service receives external traffic.

runtime:
  strategy: docker-compose
  dockerComposeFilePath: docker-compose.yml
network:
  internalPort: 8080

A deployment system that only asks which port a container listens on cannot express the full situation. There are multiple workloads, but one public entry point. The private service must be created and reachable without receiving its own public route. This example makes the control plane distinguish runtime topology from access topology.

Environment variables need a safe example

env-service demonstrates non-secret configuration. It declares APP_NAME, GREETING, and FEATURE_FLAG, then exposes safe observable values through /api/config.

We deliberately did not add database passwords or tokens. An example should show how deployment input reaches a workload without teaching users to commit secrets. Actual secrets belong in protected deployment inputs, and logs or readbacks should not return their values.

This is more useful than a README that merely says environment variables are supported. A smoke script can call the service and prove that the container received the deployment value rather than a Dockerfile default.

One smoke command, several failure boundaries

The repository has a root ./scripts/smoke-all-local.sh, and each relevant example has its own scripts/smoke-local.sh. They test more than whether a process starts:

  • Vite must build static assets before nginx can serve /health.
  • Python and Go expose incorrect port assumptions.
  • env-service verifies configuration readback.
  • Compose verifies multi-service startup, internal connectivity, and the public entry point.

Local smoke tests do not replace testing through a real deployment control plane. They do not prove that the Git source, remote server, route, or final URL is correct. They answer a cheaper question earlier: does the example work on its own? If its local container cannot pass a health check, there is little value in carrying that failure into the remote deployment path.

Examples are product interfaces

Deployment examples are often treated as documentation attachments and allowed to drift. We would rather treat them as part of the interface test suite.

Each example joins together a repository structure that users may copy, the appaloft.yml contract, a Docker build, runtime networking, and smoke verification. If an example stops working after a deployment semantic changes, the problem is larger than a broken demo. The documentation, control plane, and user repository may now disagree.

We still do not cover every application shape. Services with external databases, persistent volumes, workers, scheduled jobs, and more complicated dependency graphs should be added when there is a specific acceptance target—not to make the catalog look bigger.

These examples address a more basic problem first: a deployment platform should not look correct only when it sees its favorite kind of application. Apps differ in language, port, build process, and topology. The platform needs an explicit contract and evidence that the contract was actually carried out. We explore that second half in A deployment is successful only when the running workload changed.