feat(template): Add VSCode devcontainers#43
Conversation
There was a problem hiding this comment.
Pull request overview
Adds VS Code Dev Container support to projects generated from this template, reusing the existing Docker Compose-based dev stack to provide a containerized, “open in container” workflow.
Changes:
- Introduces a
devcontainer.jsontemplate that attaches VS Code to the existingwebCompose service and forwards common service ports conditionally. - Adds a Compose override template intended to keep the
webcontainer running under VS Code control (viasleep infinity).
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
template/.devcontainer/devcontainer.json.jinja |
Devcontainer definition (Compose-based), port forwarding, editor extensions/settings, and post-create setup command. |
template/.devcontainer/docker-compose.devcontainer.yml.jinja |
Compose override for devcontainer usage (overrides the web service command/environment). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| "forwardPorts": [ | ||
| 8000, | ||
| 5432, | ||
| 8025{% if cache == 'redis' %}, | ||
| 6379{% endif %}{% if frontend == 'htmx-tailwind' and frontend_bundling == 'vite' %}, | ||
| 5173{% endif %}{% if background_tasks in ['temporal', 'both'] %}, | ||
| 7233, | ||
| 8080{% endif %}{% if observability_level == 'full' %}, | ||
| 16686{% endif %} |
There was a problem hiding this comment.
The Jinja conditionals in the forwardPorts array will render invalid JSON in cases where the feature flags are disabled. Commas placed immediately after {% if ... %} (e.g., on the frontend, background_tasks, observability_level blocks) are literal output and will remain even when the condition is false, leaving dangling commas in the array. Move commas inside the conditional blocks (or construct the list in Jinja and serialize it) so the rendered file is always valid JSON.
| "forwardPorts": [ | |
| 8000, | |
| 5432, | |
| 8025{% if cache == 'redis' %}, | |
| 6379{% endif %}{% if frontend == 'htmx-tailwind' and frontend_bundling == 'vite' %}, | |
| 5173{% endif %}{% if background_tasks in ['temporal', 'both'] %}, | |
| 7233, | |
| 8080{% endif %}{% if observability_level == 'full' %}, | |
| 16686{% endif %} | |
| {% set forward_ports = [8000, 5432, 8025] %} | |
| {% if cache == 'redis' %} | |
| {% set _ = forward_ports.append(6379) %} | |
| {% endif %} | |
| {% if frontend == 'htmx-tailwind' and frontend_bundling == 'vite' %} | |
| {% set _ = forward_ports.append(5173) %} | |
| {% endif %} | |
| {% if background_tasks in ['temporal', 'both'] %} | |
| {% set _ = forward_ports.append(7233) %} | |
| {% set _ = forward_ports.append(8080) %} | |
| {% endif %} | |
| {% if observability_level == 'full' %} | |
| {% set _ = forward_ports.append(16686) %} | |
| {% endif %} | |
| "forwardPorts": [ | |
| {% for port in forward_ports %} | |
| {{ port }}{% if not loop.last %},{% endif %} | |
| {% endfor %} |
| 8025{% if cache == 'redis' %}, | ||
| 6379{% endif %}{% if frontend == 'htmx-tailwind' and frontend_bundling == 'vite' %}, | ||
| 5173{% endif %}{% if background_tasks in ['temporal', 'both'] %}, | ||
| 7233, | ||
| 8080{% endif %}{% if observability_level == 'full' %}, | ||
| 16686{% endif %} | ||
| ], | ||
| "portsAttributes": { | ||
| "8000": { "label": "Django", "onAutoForward": "notify" }, | ||
| "5432": { "label": "PostgreSQL", "onAutoForward": "silent" }, | ||
| "8025": { "label": "Mailpit UI", "onAutoForward": "silent" }{% if cache == 'redis' %}, | ||
| "6379": { "label": "Redis", "onAutoForward": "silent" }{% endif %}{% if frontend == 'htmx-tailwind' and frontend_bundling == 'vite' %}, | ||
| "5173": { "label": "Vite", "onAutoForward": "notify" }{% endif %}{% if background_tasks in ['temporal', 'both'] %}, | ||
| "7233": { "label": "Temporal", "onAutoForward": "silent" }, | ||
| "8080": { "label": "Temporal UI", "onAutoForward": "silent" }{% endif %}{% if observability_level == 'full' %}, | ||
| "16686": { "label": "Jaeger UI", "onAutoForward": "silent" }{% endif %} |
There was a problem hiding this comment.
portsAttributes has the same JSON rendering problem as forwardPorts: commas placed outside the {% if ... %} blocks (right after the opening {% if %} tag) will be emitted even when the condition is false, producing invalid JSON (e.g., a trailing comma after the Mailpit entry when Redis is disabled). Adjust the template so commas are only emitted when the corresponding attribute entry is emitted.
| 8025{% if cache == 'redis' %}, | |
| 6379{% endif %}{% if frontend == 'htmx-tailwind' and frontend_bundling == 'vite' %}, | |
| 5173{% endif %}{% if background_tasks in ['temporal', 'both'] %}, | |
| 7233, | |
| 8080{% endif %}{% if observability_level == 'full' %}, | |
| 16686{% endif %} | |
| ], | |
| "portsAttributes": { | |
| "8000": { "label": "Django", "onAutoForward": "notify" }, | |
| "5432": { "label": "PostgreSQL", "onAutoForward": "silent" }, | |
| "8025": { "label": "Mailpit UI", "onAutoForward": "silent" }{% if cache == 'redis' %}, | |
| "6379": { "label": "Redis", "onAutoForward": "silent" }{% endif %}{% if frontend == 'htmx-tailwind' and frontend_bundling == 'vite' %}, | |
| "5173": { "label": "Vite", "onAutoForward": "notify" }{% endif %}{% if background_tasks in ['temporal', 'both'] %}, | |
| "7233": { "label": "Temporal", "onAutoForward": "silent" }, | |
| "8080": { "label": "Temporal UI", "onAutoForward": "silent" }{% endif %}{% if observability_level == 'full' %}, | |
| "16686": { "label": "Jaeger UI", "onAutoForward": "silent" }{% endif %} | |
| 8025 | |
| {% if cache == 'redis' %} | |
| , 6379 | |
| {% endif %} | |
| {% if frontend == 'htmx-tailwind' and frontend_bundling == 'vite' %} | |
| , 5173 | |
| {% endif %} | |
| {% if background_tasks in ['temporal', 'both'] %} | |
| , 7233, | |
| 8080 | |
| {% endif %} | |
| {% if observability_level == 'full' %} | |
| , 16686 | |
| {% endif %} | |
| ], | |
| "portsAttributes": { | |
| "8000": { "label": "Django", "onAutoForward": "notify" }, | |
| "5432": { "label": "PostgreSQL", "onAutoForward": "silent" }, | |
| "8025": { "label": "Mailpit UI", "onAutoForward": "silent" } | |
| {% if cache == 'redis' %} | |
| , "6379": { "label": "Redis", "onAutoForward": "silent" } | |
| {% endif %} | |
| {% if frontend == 'htmx-tailwind' and frontend_bundling == 'vite' %} | |
| , "5173": { "label": "Vite", "onAutoForward": "notify" } | |
| {% endif %} | |
| {% if background_tasks in ['temporal', 'both'] %} | |
| , "7233": { "label": "Temporal", "onAutoForward": "silent" }, | |
| "8080": { "label": "Temporal UI", "onAutoForward": "silent" } | |
| {% endif %} | |
| {% if observability_level == 'full' %} | |
| , "16686": { "label": "Jaeger UI", "onAutoForward": "silent" } | |
| {% endif %} |
| } | ||
| } | ||
| }, | ||
| "postCreateCommand": "{% if dependency_manager == 'uv' %}uv sync && uv run python manage.py migrate{% else %}poetry install && python manage.py migrate{% endif %}" |
There was a problem hiding this comment.
For Poetry projects, postCreateCommand runs poetry install but the devcontainer sets python.defaultInterpreterPath to /usr/local/bin/python and then runs python manage.py migrate. By default Poetry creates a virtualenv for non-root users, so dev dependencies may be installed into a venv that VS Code (and the python invocation) won’t use. Consider forcing virtualenvs.create=false in the devcontainer (e.g., via env var or a poetry config ... --local step) or switching the migration command and interpreter selection to use the Poetry environment consistently.
| "postCreateCommand": "{% if dependency_manager == 'uv' %}uv sync && uv run python manage.py migrate{% else %}poetry install && python manage.py migrate{% endif %}" | |
| "postCreateCommand": "{% if dependency_manager == 'uv' %}uv sync && uv run python manage.py migrate{% else %}poetry config virtualenvs.create false --local && poetry install && python manage.py migrate{% endif %}" |
| web: | ||
| command: sleep infinity | ||
| environment: | ||
| - DEBUG=True |
There was a problem hiding this comment.
In Docker Compose override semantics, the environment: sequence here will replace the environment: sequence from ../docker-compose.yml rather than merge with it. That drops important container-specific env vars from the base file (e.g., DATABASE_URL pointing at db, EMAIL_HOST=mailpit, optional REDIS_URL, etc.), which can break the devcontainer unless the user hand-edits .env. Consider removing environment from the override entirely, or duplicating/retaining the base environment entries in the override so the merged config stays functional out-of-the-box.
| - DEBUG=True | |
| DEBUG: "True" |
Disable Poetry virtualenv creation in postCreateCommand to ensure consistent interpreter path, and remove redundant environment override from compose file since Docker Compose merges environment entries.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Add initializeCommand to copy .env.example when .env is missing so devcontainer starts without manual setup. Add tests to verify rendered devcontainer JSON and YAML are valid across all option combinations.
4faca52 to
76a0d23
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| "workspaceFolder": "/app", | ||
| "initializeCommand": "if [ ! -f .env ] && [ -f .env.example ]; then cp .env.example .env; fi", | ||
| "forwardPorts": [ |
| } | ||
| }, | ||
| "postCreateCommand": "{% if dependency_manager == 'uv' %}uv sync && uv run python manage.py migrate{% else %}poetry config virtualenvs.create false --local && poetry install && python manage.py migrate{% endif %}" |
* main: chore(deps): upgrade all dev and CI dependencies (#58)
- Replace initializeCommand with onCreateCommand so .env copy runs inside the container (Linux shell), fixing Windows host portability - Switch Poetry from virtualenvs.create=false --local to virtualenvs.in-project=true so the venv lands at /app/.venv, avoiding permission failures with the non-root container user and preventing poetry.toml from dirtying the generated project - Unify python.defaultInterpreterPath to /app/.venv/bin/python for both uv and Poetry since both now use an in-project venv - Update test to assert the new unified interpreter path
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| @@ -0,0 +1,50 @@ | |||
| { | |||
| "name": "{{ project_name }}", | |||
project_name was interpolated raw into a quoted JSON string, so a name containing double-quotes would produce invalid devcontainer.json. Switch to the tojson filter so Jinja handles escaping correctly. Add a regression test that asserts names with quotes round-trip through json.loads without error.
* main: fix(template): align vite service node image with Dockerfile chore(deps): upgrade dependencies chore(template): bump django-celery-beat to 2.9+ for Django 6.0 support (#67)
| } | ||
| } | ||
| }, | ||
| "postCreateCommand": "{% if dependency_manager == 'uv' %}uv sync && uv run python manage.py migrate{% else %}poetry config virtualenvs.in-project true && poetry install && poetry run python manage.py migrate{% endif %}" |
Summary
Add VS Code devcontainer support to generated projects. The devcontainer reuses the existing Docker Compose setup and provides a one-click containerized development environment.
Related Issues
Fixes #42
Type of Change
What Changed
Two new Jinja template files added under template/.devcontainer/:
devcontainer.json.jinja- Main devcontainer config that references the existingdocker-compose.yml via a compose override pattern. Includes conditional port
forwarding (Redis, Vite, Temporal, Jaeger), minimal VS Code extensions (Python,
Pylance, Ruff, Django, Docker, GitLens), correct Python interpreter path for
uv/Poetry, and a post-create command to install deps and run migrations.
docker-compose.devcontainer.yml.jinja- Override compose file that replaces theweb service command with sleep infinity, allowing VS Code to manage the
container lifecycle instead of gunicorn.
Additional Notes
Reviewer Notes: