fix: switch freeform DDEV router to port 8080, fix coder-routes and ddev launch, fixes #87#115
fix: switch freeform DDEV router to port 8080, fix coder-routes and ddev launch, fixes #87#115
Conversation
…dev launch, closes #87 - Set ddev config global --router-http-port=8080 in freeform startup script; port 80 conflicts with Coder's own proxy and is excluded from auto port-forwarding - Update coder_app.ddev-web to port 8080 (url and healthcheck) - coder-routes: detect web service on ext_port 8080 (was 80); sanitize DDEV project name as DNS-safe PROJECT_SLUG for per-project URL differentiation; write per-project coder-routes-{project}.yaml files so multiple projects coexist without overwriting - launch: remove hardcoded 80--agent--workspace--owner URL (wrong format); read all routes from per-project coder-routes file and derive correct URLs from Host/PathPrefix rules; falls back to legacy coder-routes.yaml for existing setups - coder-setup: add post-stop hook to remove per-project routes file on project stop Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds four steps to the freeform integration test: - Start two PHP projects (ci-site1/ci-site2) with ddev coder-setup - Verify ddev launch shows the correct per-project Coder subdomain URL for each (catches slug mix-ups and coder-routes file overwrites) - Verify ddev describe shows the correct .ddev.site URL for each project - Cleanup both projects before workspace deletion Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ddev delete with a positional project name triggers the TUI project selector in DDEV v1.25.x when not in a project directory. Use cd /tmp/PROJ && ddev delete instead to give DDEV unambiguous context. Also add NO_COLOR to the cleanup env. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
coder ssh joins post-'--' args with spaces before the remote shell parses them, so bash -c "multi word" becomes bash -c multi word — bash receives only the first word as its script and runs ddev bare, triggering the TUI. - Verify ddev launch: pass DDEV_SITENAME env var so the launch script finds the right project without needing cd - Verify ddev describe: use 'ddev describe <name>' (accepts project arg) - Cleanup: use 'ddev delete <name> -Oy' directly, no bash -c Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When ddev launch is invoked as a global command outside a project directory, DDEV overwrites DDEV_SITENAME with empty (no project found in CWD). Run the script directly via bash so our DDEV_SITENAME env var is preserved and the routes file is found. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ddev launch must run in project context (cd into project dir). The only safe way to do that through coder ssh without bash -c quoting issues is the same scp-script pattern used by the start step. Combines ddev launch and ddev describe checks into one verify step. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Move inline heredoc scripts to freeform/scripts/ so they can be run manually in a workspace or by CI. Each script accepts a suffix arg (github.run_id in CI, any string manually) and derives workspace/owner/domain from Coder agent env vars when not provided. - test-freeform-start.sh create and start two PHP test projects - test-freeform-verify.sh check ddev launch and ddev describe URLs - test-freeform-cleanup.sh delete both projects Workflow steps are now SCP + bash /tmp/script.sh, no inline heredocs. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two bugs in coder-routes revealed by the multi-project CI test: 1. The ext_port="8080" check never fired because DDEV names its Traefik entrypoint after the internal web port (http-80), not the globally configured router-http-port (8080). Switch to detecting the primary web by slug=PROJECT_SLUG, which is set semantically when svc_name=web and port≠8025. 2. DDEV's _merged.yaml includes routers from all running projects. Add a service-prefix guard to skip any service not belonging to the current DDEV project, preventing ci-site2's routes from appearing in ci-site1's coder-routes file. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… tests Ensures CI always tests coder-routes, coder-setup, and launch from the current branch rather than the stale image baked at workspace start time. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…et 24h default TTL - coder-routes: write .ddev/docker-compose.coder-describe.yaml with x-ddev.describe-url-port labels so 'ddev describe' shows Coder URLs - coder-setup: also gitignore docker-compose.coder-describe.yaml - WELCOME.txt: point to github.com/ddev/coder-ddev, drop ~/projects, use <yourdir> placeholder to clarify any directory name works - freeform/README.md: same <yourdir> fix, correct coder-routes filename to coder-routes-<project>.yaml, document new describe file - freeform/template.tf: use <yourdir> in startup Next steps output - Makefile: set --default-ttl 24h for freeform template Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Suppresses DDEV's "Custom configuration detected" warnings for the Coder-managed files that are intentionally installed in every workspace. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ter Docker starts coder-routes: primary web URL must use WORKSPACE (= coder_app slug) as the first segment so Coder's subdomain proxy can match it. Using PROJECT_SLUG broke routing when the DDEV project name differed from the workspace name. freeform/template.tf: move 'ddev config global --router-http-port=8080' to after the Docker socket is ready — DDEV config global needs Docker running and was silently failing when called before dockerd started. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…RL in ddev describe
Previous test checked for PROJECT_SLUG as the URL first segment, which matched
the (broken) coder-routes output and masked the bug. All projects in a freeform
workspace share the coder_app slug (workspace name), so the expected URL is
always workspace--workspace--owner.domain.
Also replace the ddev describe check: was testing for {proj}.ddev.site (internal
DDEV URL, always present) instead of the actual Coder URL.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…n to coder-routes YAML, fix verify test - Replace set +e with set -euo pipefail; add || true on glob expansions and npm config - Prepend #ddev-silent-no-warn to Traefik YAML written by coder-routes to suppress DDEV warnings - Fix verify test: ddev describe does not show Coder URL (DDEV ignores x-ddev.describe-url-port on web service); check for running status instead; ddev launch remains the canonical Coder URL command Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Use a custom service with profiles:["coder-url"] and x-ddev.describe-url-port in docker-compose.coder-describe.yaml. DDEV ignores x-ddev on the built-in web service but renders it for custom services. The coder-url service shows as "stopped" (like xhgui) with the Coder URL in the URL/PORT column and "Use: ddev launch" in INFO. URLs appear in ddev describe after the next ddev start (one restart needed to pick up the compose file written by coder-routes in the post-start hook). Update test-freeform-verify.sh to restart before checking ddev describe and verify the Coder URL is present. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Multi-project routing design notesWhile working on this PR we researched how Coder constructs workspace URLs and identified the path to supporting multiple concurrent DDEV projects in one workspace. Notes here for future reference. How Coder subdomain URLs are builtFor a For dashboard port-forwarding (no Where The Coder docs don't document the named-app URL pattern in one place; the closest reference is coder.com/docs/admin/networking/wildcard-access-url. Why multi-project is currently one-at-a-timeThe freeform template has one Recommended approach for multi-project supportUse a mutable Terraform parameter for comma-separated project names and generate data "coder_parameter" "project_names" {
name = "project_names"
display_name = "DDEV project names"
description = "Comma-separated DDEV project names. Each gets its own app button and URL."
default = data.coder_workspace.me.name
type = "string"
mutable = true
}
locals {
project_names = [for s in split(",", data.coder_parameter.project_names.value) : trimspace(s)]
}
resource "coder_app" "ddev_web" {
for_each = toset(local.project_names)
agent_id = coder_agent.main.id
slug = each.key
display_name = each.key
url = "http://localhost:8080"
subdomain = true
share = "owner"
}All apps point to The naming contract: the DDEV project name must equal the Single-project users: the default is the workspace name and Adding a second project later: because |
- Add project_names parameter (mutable, comma-separated, defaults to workspace name) generating one coder_app per project via for_each - Add CODER_PROJECT_NAMES env var so startup script and coder-setup can reference the registered project list - Update startup script next-steps to show required project names and the --project-name flag - Update coder-setup to warn when DDEV project name is not in the registered list, explaining how to fix it - Update coder-routes to use PROJECT_SLUG (= DDEV project name) as the URL slug rather than always using WORKSPACE; this makes the Traefik Host rule match the coder_app slug for each project - Update WELCOME.txt to show the naming contract - Add Terraform tests for single-project default, two-project, and whitespace-trimming cases - Add docs/reference/coder-url-patterns.md reference document - Document scp-based script execution technique in CLAUDE.md - Bump VERSION to v0.4 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… use per-project URLs in verify
- Create freeform workspace with project_names=ci-site1-<run_id>,ci-site2-<run_id>
via --rich-parameter-file to avoid comma-as-separator issue with --parameter
- test-freeform-verify.sh: EXPECTED_URL now uses project slug (${PROJ}--${WORKSPACE}--...)
instead of the old workspace-name URL shared across all projects
- Cleanup step: replace blanket || true with explicit coder show existence check so
real cleanup failures are visible; gracefully skip when workspace was never created
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…launch output Previously yq returned routers alphabetically so xhgui appeared before Web. Collect each category into a variable and print in priority order. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…drop busybox No need for a separate coder-url service with a busybox image and profiles. Attaching x-ddev labels directly to the web service is sufficient. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Check Mailpit URL in ddev launch output (was only checking Web) - Check both Web and Mailpit URLs in ddev describe output - Drop stale ddev restart step — post-start hook writes the describe file during ddev start so no restart is needed Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…oid sharing
A single coder_app "mailpit" slug meant all projects in a workspace routed to
mailpit--workspace--owner.domain — whichever project ran coder-routes last won.
Now coder_app.mailpit is for_each on project_names with slug "mailpit-{project}",
and coder-routes emits "mailpit-{PROJECT_SLUG}" as the Traefik router slug to match.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…er-routes post-start DDEV reads docker-compose files at start time, so the file must exist before ddev start. Writing it in the post-start hook (coder-routes) meant it was never picked up until the following start. coder-setup now writes the file with statically-computed URLs immediately after writing config.coder.yaml, before the user runs ddev start. coder-routes no longer writes or manages this file. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…nflicts
xhgui is always present in the image; adminer is opt-in but per-project when
enabled. Both use shared Traefik entrypoint ports where name-based routing means
multiple projects would conflict with a single slug.
- coder_app.xhgui: for_each on project_names, slug xhgui-{project}
- coder_app.adminer: for_each on project_names (when enable_adminer=true), slug adminer-{project}
- coder-routes: detect xhgui/adminer by svc_name and emit Host() rules with per-project slugs
Unknown add-ons in multi-project workspaces still use PathPrefix/port-forwarding
and will conflict on shared ports — documented limitation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Builds linux/amd64 on native GitHub-hosted runners, eliminating the need to cross-compile from arm64 or SSH into a remote machine to build. Triggers on push to main (image/ or VERSION changes) and workflow_dispatch. PRs build but do not push. Uses GitHub Actions cache for layer caching. Requires DOCKERHUB_USERNAME and DOCKERHUB_TOKEN repository secrets. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…2 chars Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Summary
ddev config global --router-http-port=8080in freeform startup script; port 80 conflicts with Coder's own proxy and is excluded from auto port-forwardingcoder_app.ddev-webto port 8080 (url and healthcheck)coder-routes: detect web service on ext_port 8080 (was 80); sanitize DDEV project name as DNS-safePROJECT_SLUGfor per-project URL differentiation; write per-projectcoder-routes-{project}.yamlfiles so multiple projects coexist without overwritinglaunch: remove hardcoded wrong-format URL; read all routes from per-project coder-routes file and derive correct URLs from Host/PathPrefix rules; falls back to legacycoder-routes.yamlfor existing setupscoder-setup: add post-stop hook to remove per-project routes file on project stopCloses #87
Test plan
ddev-webapp in Coder dashboard opens the site correctlyddev coder-routesand verifycoder-routes-{project}.yamlis createdddev launchand verify correct URLs are printed/opened🤖 Generated with Claude Code