Skip to content

Docker deploy: give apps a writable /data, and stop reissuing ports after a restart - #575

Open
lmo-selected wants to merge 1 commit into
yc-software:mainfrom
lmo-selected:deploy-docker-data-and-ports
Open

Docker deploy: give apps a writable /data, and stop reissuing ports after a restart#575
lmo-selected wants to merge 1 commit into
yc-software:mainfrom
lmo-selected:deploy-docker-data-and-ports

Conversation

@lmo-selected

@lmo-selected lmo-selected commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Two defects in the Docker deploy provider. Together they make a locally deployed app that stores anything fail on first run, and then fail differently after a restart. Both are invisible from the outside: the deployment record says running while the container has already exited.

1. No writable storage

The container gets the snapshot mounted read-only at /app and nothing writable anywhere:

-v ${version.snapshotDir}:/app:ro

So an app that opens a database or writes a file dies on startup. mkdirSync('/app/data') fails, and with { recursive: true } it fails the same way, just with EROFS instead of ENOENT.

The platform already has a contract for this. aws-deploy-provider.ts defines DATA_DIR = "/data" and DATA_DB = "/data/app.db", and wiring.ts warns that an AWS deployment without a data bucket has "NO durable /data". DeployProfile even carries a dataDir field. The Docker provider never implemented its half — so an app written correctly against that contract works on AWS and crashes locally, which is the wrong way round for the environment people develop in.

Now: a per-deployment directory on the host, mounted read-write at /data, DATA_DIR in the app's environment, and the path advertised on profile.dataDir. The directory lives outside the container, so data survives a redeploy.

2. Host ports are allocated in memory

let nextPort = opts.basePort ?? 9200;
const ports = new Map<string, number>();

Every process restart resets the counter to the base. The next apply then hands out a port an existing container already holds, and the second container to start fails to bind — after several restarts a set of deployments can all carry the same recorded port.

The allocation is already durable: it is on the deployment record as endpoint.port, and apply receives the Deployment. So this keeps no new state — apply prefers the recorded port and advances the counter past it, instead of maintaining a parallel copy in RAM that a restart silently invalidates.

Tests

dataRoot and mkdir are injectable alongside the existing dockerExec, so both paths are covered without touching a real filesystem or daemon:

  • the data directory is created, mounted read-write at /data, and DATA_DIR is set, while the snapshot stays :ro
  • a restart, modelled as a fresh provider instance over the same records: a redeploy keeps its recorded port, and a subsequent new deployment does not collide with it

test/docker-deploy-provider.test.ts 7/7, test/aws-deploy-provider.test.ts 46/46 unaffected, tsc --noEmit clean.

Not fixed here

Nothing reconciles a deployment's recorded status against the container's actual state, so a crashed app reports running indefinitely and the UI shows it as starting up. That is what made both defects above hard to see, but it needs a reconcile loop rather than a change to this file, so it is left for a separate change.


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

…tart

Two defects that together make a locally deployed app that stores anything
fail on first run and then fail differently after a restart.

The container mounts the snapshot read-only at /app and nothing writable
anywhere, so an app that opens a database or writes a file dies on startup. The
platform already has a contract for this: aws-deploy-provider sets DATA_DIR to
/data, and wiring warns that a deployment without a data bucket has "NO durable
/data". The Docker provider never implemented its half, so an app written
against that contract works on AWS and crashes locally. It now gets a
per-deployment host directory mounted at /data, DATA_DIR in its environment, and
the same path advertised on the provider profile. The directory lives outside the
container, so data survives a redeploy.

Host ports were handed out from a counter held in process memory. Every restart
reset it to the base, so the next deployment was handed a port an existing
container already held, and the second one to start failed to bind. The
allocation is already durable — it is on the deployment record as endpoint.port
— so apply now prefers the recorded port and advances the counter past it,
rather than keeping a parallel copy in RAM that a restart silently invalidates.

Tests cover the mount, the environment variable, the read-only snapshot, and a
restart modelled as a fresh provider instance: a redeploy keeps its port, and a
new deployment does not collide with it.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant