feat(docker): implement site container creation #655

Merged
fuzzy merged 3 commits from feat/docker-container-create into main 2026-08-13 16:34:23 +00:00
Owner

What

Implement site container creation for standalone mode (Phase 2, first task). Adds the container-creation primitive to internal/docker and the static-server image config.

  • Static image (#344) — new standalone.static_image config (default nginx:alpine); wired together with standalone.base_dir into StandaloneDeployer.
  • Directory mount (#345) — site assets are bind-mounted read-only at /usr/share/nginx/html (per the standalone deployment model's storage resolution; host directories, no named volumes).
  • Container naming (#346)tpagectl-{site-name}-{version} for production, tpagectl-{site-name}-preview-{branch} for previews, with documented sanitization (invalid chars → -, collapse/strip/truncate).
  • Tracking labels (#347)tpagectl.site, tpagectl.version (preview for previews), tpagectl.preview_branch (previews only), tpagectl.deployed_at (RFC3339).
  • Restart policy (#348)unless-stopped for production, on-failure for previews.

StandaloneDeployer.PublishSite remains not-implemented; the primitive is consumed by Phase 4 deployment wiring (#393), since the Deployer interface does not yet carry a version.

Why

Phase 2 begins container lifecycle management. This task delivers the container-creation capability (image/mount/name/labels/restart policy) that Phase 2 removal (#353), inspection (#357), and Phase 4 deployment build on.

Testing

  • internal/docker — fake unix-socket daemon answering POST /containers/create and /containers/{id}/start
  • Assert create payload: image, labels (site/version/preview/deployed_at), read-only bind mount (source/target/type), restart policy (unless-stopped vs on-failure)
  • Production vs preview name + labels
  • Error propagation: create failure, start failure
  • containerName / sanitizeName table tests (invalid chars, collapse, strip, truncate, missing fields)
  • internal/configstatic_image default + YAML/TOML/env fixtures
  • go vet ./... clean; go test -race -count=1 ./... full suite, no failures
  • Pre-commit hooks pass (golangci-lint, go fmt, go mod tidy, go test, gitleaks)

Breaking Changes

None. New config key; constructor signature of docker.NewStandaloneDeployer extended internally.

Notes

ContainerCreate is invoked with a nil platform argument (host default). Bind mounts follow the design doc's host-directory storage resolution; Docker named volumes are not used in standalone mode.

Closes #344
Closes #345
Closes #346
Closes #347
Closes #348
Closes #349

## What Implement site container creation for standalone mode (Phase 2, first task). Adds the container-creation primitive to `internal/docker` and the static-server image config. - **Static image (#344)** — new `standalone.static_image` config (default `nginx:alpine`); wired together with `standalone.base_dir` into `StandaloneDeployer`. - **Directory mount (#345)** — site assets are bind-mounted **read-only** at `/usr/share/nginx/html` (per the standalone deployment model's storage resolution; host directories, no named volumes). - **Container naming (#346)** — `tpagectl-{site-name}-{version}` for production, `tpagectl-{site-name}-preview-{branch}` for previews, with documented sanitization (invalid chars → `-`, collapse/strip/truncate). - **Tracking labels (#347)** — `tpagectl.site`, `tpagectl.version` (`preview` for previews), `tpagectl.preview_branch` (previews only), `tpagectl.deployed_at` (RFC3339). - **Restart policy (#348)** — `unless-stopped` for production, `on-failure` for previews. `StandaloneDeployer.PublishSite` remains not-implemented; the primitive is consumed by Phase 4 deployment wiring (#393), since the `Deployer` interface does not yet carry a version. ## Why Phase 2 begins container lifecycle management. This task delivers the container-creation capability (image/mount/name/labels/restart policy) that Phase 2 removal (#353), inspection (#357), and Phase 4 deployment build on. ## Testing - [x] `internal/docker` — fake unix-socket daemon answering `POST /containers/create` and `/containers/{id}/start` - [x] Assert create payload: image, labels (site/version/preview/deployed_at), read-only bind mount (source/target/type), restart policy (unless-stopped vs on-failure) - [x] Production vs preview name + labels - [x] Error propagation: create failure, start failure - [x] `containerName` / `sanitizeName` table tests (invalid chars, collapse, strip, truncate, missing fields) - [x] `internal/config` — `static_image` default + YAML/TOML/env fixtures - [x] `go vet ./...` clean; `go test -race -count=1 ./...` full suite, no failures - [x] Pre-commit hooks pass (golangci-lint, go fmt, go mod tidy, go test, gitleaks) ## Breaking Changes None. New config key; constructor signature of `docker.NewStandaloneDeployer` extended internally. ## Notes `ContainerCreate` is invoked with a nil platform argument (host default). Bind mounts follow the design doc's host-directory storage resolution; Docker named volumes are not used in standalone mode. Closes #344 Closes #345 Closes #346 Closes #347 Closes #348 Closes #349
feat(docker): implement site container creation
Some checks failed
CI / docker (push) Failing after 54s
CI / build (push) Successful in 5m42s
CI / test (push) Successful in 5m46s
CI / docker (pull_request) Successful in 3m2s
CI / test (pull_request) Successful in 10m5s
CI / build (pull_request) Successful in 6m11s
CI / lint (push) Successful in 14m13s
CI / lint (pull_request) Successful in 6m3s
36bbbd535e
Add the container-creation primitive for standalone mode, creating and
starting a static-server container per deployment.

- config: add standalone.static_image (default nginx:alpine); wire it and
  base_dir into the standalone deployer
- docker: StandaloneDeployer.CreateSiteContainer builds the container
  config (image, tracking labels, read-only bind mount of the site
  directory at /usr/share/nginx/html) and start policy (unless-stopped
  production, on-failure previews); containerName follows the
  tpagectl-{site}-{version} / tpagectl-{site}-preview-{branch} convention
  with documented sanitization
- tests: fake unix-socket daemon answers container create/start; asserts
  image, name, labels, bind mount, restart policy, and error propagation
- DOCKER-ROADMAP.md: mark #349 and subtasks complete

Closes #344
Closes #345
Closes #346
Closes #347
Closes #348
Closes #349
fuzzy scheduled this pull request to auto merge when all checks succeed 2026-08-13 15:42:28 +00:00
the.auditor approved these changes 2026-08-13 15:44:40 +00:00
Dismissed
the.auditor left a comment

Solid Phase 2 primitive. The create call is correct against the SDK (ContainerCreate with nil platform, fixed name), bind mount is read-only at /usr/share/nginx/html, restart policies and tracking labels match the approved deployment model, and sanitization is consistent with site-name validation (lowercase DNS labels enforced at registration, so no lowercase gap). The fake-daemon tests are valid — version negotiation goes through /_ping, so the versioned create/start paths match — and cover the payload, labels, mount, restart policy, error propagation, and naming. Referenced issues #344–#349 are all closed and the roadmap checkboxes match.

Suggestions

  1. internal/docker/container_test.gotruncateName (255-char limit) is never exercised; TestContainerName/TestSanitizeName stop short of the truncation path. Add a long-name boundary test. Filed as #656.
  2. internal/docker/container.goSiteDir(s, version) has no preview counterpart ({baseDir}/sites/{site}/preview-{branch}); Phase 4 preview wiring will need it. Consider a PreviewDir(s, branch) helper. Filed as #657.

Questions

  • Containers are created with an empty network.NetworkingConfig{}, so they join the default bridge rather than tpagectl-net (the model doc's Create step says attached to tpagectl-net). Consistent with roadmap sequencing — attach lands with #378 in Phase 3 — but containers created before then will need recreation to join the shared network.
  • Fixed-name create returns 409 when a container with that name already exists; re-deploying the same version requires Phase 4 to remove-or-reuse first.

No blocking issues. Approving.

Solid Phase 2 primitive. The create call is correct against the SDK (`ContainerCreate` with nil platform, fixed name), bind mount is read-only at `/usr/share/nginx/html`, restart policies and tracking labels match the approved deployment model, and sanitization is consistent with site-name validation (lowercase DNS labels enforced at registration, so no lowercase gap). The fake-daemon tests are valid — version negotiation goes through `/_ping`, so the versioned create/start paths match — and cover the payload, labels, mount, restart policy, error propagation, and naming. Referenced issues #344–#349 are all closed and the roadmap checkboxes match. ## Suggestions 1. `internal/docker/container_test.go` — `truncateName` (255-char limit) is never exercised; `TestContainerName`/`TestSanitizeName` stop short of the truncation path. Add a long-name boundary test. Filed as #656. 2. `internal/docker/container.go` — `SiteDir(s, version)` has no preview counterpart (`{baseDir}/sites/{site}/preview-{branch}`); Phase 4 preview wiring will need it. Consider a `PreviewDir(s, branch)` helper. Filed as #657. ## Questions - Containers are created with an empty `network.NetworkingConfig{}`, so they join the default bridge rather than `tpagectl-net` (the model doc's Create step says attached to `tpagectl-net`). Consistent with roadmap sequencing — attach lands with #378 in Phase 3 — but containers created before then will need recreation to join the shared network. - Fixed-name create returns 409 when a container with that name already exists; re-deploying the same version requires Phase 4 to remove-or-reuse first. No blocking issues. Approving.
fuzzy canceled auto merging this pull request when all checks succeed 2026-08-13 15:52:35 +00:00
Add StandaloneDeployer.PreviewDir so preview asset paths
({baseDir}/sites/{site}/preview-{branch}) stay next to the production
SiteDir form, ready for Phase 4 preview wiring.

Closes #657
test(docker): cover container name truncation and asset dirs
Some checks failed
CI / test (pull_request) Successful in 6m0s
CI / docker (pull_request) Successful in 3m11s
CI / build (pull_request) Successful in 6m21s
CI / lint (pull_request) Successful in 6m9s
CI / test (push) Has been cancelled
CI / build (push) Has been cancelled
CI / docker (push) Has been cancelled
CI / lint (push) Has been cancelled
5c7bb8e44e
Add boundary coverage for the 255-char Docker name limit (short
pass-through, long truncation, and an end-to-end containerName case that
exceeds the limit), plus SiteDir/PreviewDir path assertions matching the
deployment model layout.

Closes #656
fuzzy dismissed the.auditor's review 2026-08-13 16:10:34 +00:00
Reason:

New commits pushed, approval review dismissed automatically according to repository settings

Author
Owner

Review feedback addressed:

  • #656 (truncateName coverage): TestTruncateName (short pass-through + long truncation to the 255-char limit) and TestContainerNameTruncatesLongName (end-to-end name exceeding the limit via containerName). Commit 5c7bb8e.
  • #657 (PreviewDir helper): added StandaloneDeployer.PreviewDir(s, branch) returning {baseDir}/sites/{site}/preview-{branch}, mirroring SiteDir. Commit efd9fcf.

On the two questions:

  • Default bridge vs tpagectl-net: containers intentionally start on the default bridge. Shared-network attachment is Phase 3 (#374/#375/#378); containers created before then would need recreation to join tpagectl-net. Noted in the deployment model doc; deferred by design.
  • 409 on re-create of same name: fixed-name create returns 409 when a container already exists; remove-or-reuse before re-deploying the same version is Phase 4 (#350/#395). Deferred by roadmap sequencing.
Review feedback addressed: - **#656** (truncateName coverage): `TestTruncateName` (short pass-through + long truncation to the 255-char limit) and `TestContainerNameTruncatesLongName` (end-to-end name exceeding the limit via `containerName`). Commit `5c7bb8e`. - **#657** (PreviewDir helper): added `StandaloneDeployer.PreviewDir(s, branch)` returning `{baseDir}/sites/{site}/preview-{branch}`, mirroring `SiteDir`. Commit `efd9fcf`. On the two questions: - **Default bridge vs `tpagectl-net`**: containers intentionally start on the default bridge. Shared-network attachment is Phase 3 (#374/#375/#378); containers created before then would need recreation to join `tpagectl-net`. Noted in the deployment model doc; deferred by design. - **409 on re-create of same name**: fixed-name create returns 409 when a container already exists; remove-or-reuse before re-deploying the same version is Phase 4 (#350/#395). Deferred by roadmap sequencing.
the.auditor left a comment

Re-review of commit 5c7bb8e. Both suggestions are addressed:

  • #656TestTruncateName (short pass-through, long name cut to exactly 255) and TestContainerNameTruncatesLongName (end-to-end through containerName, prefix preserved, length == 255) cover the truncation path. Closed.
  • #657StandaloneDeployer.PreviewDir(s, branch) returns {baseDir}/sites/{site}/preview-{branch}, mirroring SiteDir, verified by TestStandaloneDeployerDirs. Closed.

Both questions were answered with documented roadmap-sequenced deferrals (shared-network attach → Phase 3 #374/#375/#378; re-create handling → Phase 4 #350/#395), which is consistent with the deployment model.

No blocking issues and no new findings. Approving.

Re-review of commit 5c7bb8e. Both suggestions are addressed: - **#656** — `TestTruncateName` (short pass-through, long name cut to exactly 255) and `TestContainerNameTruncatesLongName` (end-to-end through `containerName`, prefix preserved, length == 255) cover the truncation path. Closed. - **#657** — `StandaloneDeployer.PreviewDir(s, branch)` returns `{baseDir}/sites/{site}/preview-{branch}`, mirroring `SiteDir`, verified by `TestStandaloneDeployerDirs`. Closed. Both questions were answered with documented roadmap-sequenced deferrals (shared-network attach → Phase 3 #374/#375/#378; re-create handling → Phase 4 #350/#395), which is consistent with the deployment model. No blocking issues and no new findings. Approving.
fuzzy scheduled this pull request to auto merge when all checks succeed 2026-08-13 16:33:03 +00:00
fuzzy merged commit 5c7bb8e44e into main 2026-08-13 16:34:23 +00:00
fuzzy deleted branch feat/docker-container-create 2026-08-13 16:34:23 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
3 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
thwap/thwap-pagesd!655
No description provided.