Skip to content

fix: switch freeform DDEV router to port 8080, fix coder-routes and ddev launch, fixes #87#115

Open
rfay wants to merge 28 commits intomainfrom
fix/87-port-8080-routing
Open

fix: switch freeform DDEV router to port 8080, fix coder-routes and ddev launch, fixes #87#115
rfay wants to merge 28 commits intomainfrom
fix/87-port-8080-routing

Conversation

@rfay
Copy link
Copy Markdown
Member

@rfay rfay commented May 5, 2026

Summary

  • 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 wrong-format URL; 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

Closes #87

Test plan

  • Start a freeform workspace and verify DDEV starts on port 8080
  • Confirm the ddev-web app in Coder dashboard opens the site correctly
  • Run ddev coder-routes and verify coder-routes-{project}.yaml is created
  • Run ddev launch and verify correct URLs are printed/opened
  • Stop project and verify per-project routes file is removed
  • Test with multiple DDEV projects in same workspace to confirm no route file overwriting

🤖 Generated with Claude Code

rfay and others added 15 commits May 5, 2026 04:21
…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>
@rfay
Copy link
Copy Markdown
Member Author

rfay commented May 5, 2026

Multi-project routing design notes

While 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 built

For a coder_app with subdomain = true and a single agent (the normal case):

https://{slug}--{workspace}--{owner}.{wildcard-domain}/

For dashboard port-forwarding (no coder_app declaration needed):

https://{port}--{agent}--{workspace}--{owner}.{wildcard-domain}/

Where {agent} is the Terraform resource label on coder_agent — in this template that's always main.

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-time

The freeform template has one coder_app with slug = workspace_name. coder-routes always constructs the primary web URL as {workspace}--{workspace}--{owner}.{domain}, so whichever DDEV project ran last wins that URL.

Recommended approach for multi-project support

Use a mutable Terraform parameter for comma-separated project names and generate coder_app resources via for_each:

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 http://localhost:8080 (ddev-router). ddev-router routes by Host: header — each coder_app gets a different subdomain, and ddev-router dispatches to the right DDEV container.

The naming contract: the DDEV project name must equal the coder_app slug. coder-routes derives the Traefik Host() rule from the DDEV project name, so drupalHost(\drupal--myworkspace--rfay.coder.ddev.com`)which matches the URL Coder generates forslug = "drupal"`. No extra configuration needed; names just have to align.

Single-project users: the default is the workspace name and ddev config defaults the project name to the directory name, so naming the project directory after the workspace keeps everything aligned without any thought.

Adding a second project later: because mutable = true, users can edit the parameter via the Coder dashboard or coder update myworkspace, add a name, and Coder runs terraform apply to add the new coder_app — no rebuild, no data loss.

rfay and others added 6 commits May 5, 2026 16:19
- 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>
@rfay rfay changed the title fix: switch freeform DDEV router to port 8080, fix coder-routes and ddev launch fix: switch freeform DDEV router to port 8080, fix coder-routes and ddev launch, fixes #87 May 5, 2026
rfay and others added 7 commits May 5, 2026 17:06
…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>
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.

"freeform" template struggles with port 80, switch to 8080

1 participant