Conversation
This comment was marked as outdated.
This comment was marked as outdated.
| CMD ["uv", "run", "gunicorn", \ | ||
| "--timeout", "300", \ | ||
| "--capture-output", \ | ||
| "--access-logfile", "-", \ | ||
| "--error-logfile", "-", \ | ||
| "--log-level", "info", \ | ||
| "-w", "4", \ | ||
| "-b", "0.0.0.0:5000", \ | ||
| "tenantfirstaid.app:app"] |
There was a problem hiding this comment.
Production Readiness: Missing health check endpoint
The Gunicorn configuration looks solid, but consider adding health check configuration and signal handling:
# Add healthcheck
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD curl -f http://localhost:5000/health || exit 1
# Improve graceful shutdown
CMD ["uv", "run", "gunicorn", \
"--timeout", "300", \
"--graceful-timeout", "30", \
"--keep-alive", "5", \
"--capture-output", \
"--access-logfile", "-", \
"--error-logfile", "-", \
"--log-level", "info", \
"-w", "4", \
"-b", "0.0.0.0:5000", \
"tenantfirstaid.app:app"]This requires adding a /health endpoint to the Flask app.
| try_files $uri /index.html; | ||
| } | ||
|
|
||
| # Security headers |
There was a problem hiding this comment.
Security: Add CSP and additional hardening headers
Good security headers present, but consider adding Content Security Policy and additional hardening:
# Content Security Policy
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self' http://localhost:5000;" always;
# Additional security headers
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;Note: You may need to adjust CSP based on actual frontend requirements (external fonts, CDN assets, etc.).
| } | ||
| EOF | ||
|
|
||
| EXPOSE 80 |
There was a problem hiding this comment.
Production Readiness: Add healthcheck and proper logging
Consider adding nginx healthcheck and improving logging configuration:
HEALTHCHECK --interval=30s --timeout=3s \
CMD wget --quiet --tries=1 --spider http://localhost/health || exit 1
# Configure nginx for better logging
RUN cat > /etc/nginx/nginx.conf <<'EOF'
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
keepalive_timeout 65;
gzip on;
include /etc/nginx/conf.d/*.conf;
}
EOF| - "5000:5000" | ||
| volumes: | ||
| # Mount source for hot reload | ||
| - ./backend/tenantfirstaid:/app/tenantfirstaid:ro |
There was a problem hiding this comment.
Development Experience: Read-only volumes limit functionality
The :ro (read-only) flag on source volumes prevents some development workflows:
- Can't run formatters/linters that modify files in place
- Can't generate coverage reports
- Can't create new test files from within container
Recommendation: Remove :ro for development volumes or make it configurable:
volumes:
- ./backend/tenantfirstaid:/app/tenantfirstaid # writable for dev tools
- ./backend/tests:/app/tests
- ./backend/scripts:/app/scriptsFor security-sensitive mounts (credentials), keep :ro.
| - VERTEX_AI_DATASTORE=${VERTEX_AI_DATASTORE} | ||
| - MODEL_NAME=${MODEL_NAME:-gemini-2.5-pro} | ||
| - LOG_LEVEL=${LOG_LEVEL:-DEBUG} | ||
| - LANGSMITH_API_KEY=${LANGSMITH_API_KEY} |
There was a problem hiding this comment.
Configuration: Verify LangSmith defaults
LangSmith tracing is enabled by default in the dev environment. This could:
- Generate significant API usage/costs if developers aren't aware
- Send data to external service during local dev
- Cause issues if LANGSMITH_API_KEY is not set
Recommendation:
- LANGSMITH_TRACING=${LANGSMITH_TRACING:-false} # Opt-in instead of opt-out
- LANGCHAIN_TRACING_V2=${LANGCHAIN_TRACING_V2:-false}Update .env.docker.example to document when developers should enable this.
| ports: | ||
| - "127.0.0.1:5000:5000" |
There was a problem hiding this comment.
Production Configuration: Bind to localhost only
Good security practice binding to 127.0.0.1 (prevents direct external access), but this assumes:
- A reverse proxy (nginx, traefik) will handle external traffic
- The proxy runs on the same host or has network access
Recommendation: Document this requirement in Architecture.md and consider adding the reverse proxy to the compose file:
services:
nginx-proxy:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
depends_on:
- frontend
- backend| environment: | ||
| - ENV=prod | ||
| - FLASK_ENV=production | ||
| - FLASK_SECRET_KEY=${FLASK_SECRET_KEY} |
There was a problem hiding this comment.
Security: FLASK_SECRET_KEY in environment
Passing FLASK_SECRET_KEY via environment variable is less secure than using Docker secrets for production.
Recommendation:
secrets:
flask_secret:
external: true
services:
backend:
secrets:
- flask_secret
environment:
- FLASK_SECRET_KEY_FILE=/run/secrets/flask_secretThen update Flask app to read from file:
secret_key_file = os.getenv('FLASK_SECRET_KEY_FILE')
if secret_key_file:
with open(secret_key_file) as f:
app.config['SECRET_KEY'] = f.read().strip()| LOG_LEVEL=DEBUG | ||
|
|
||
| # Flask configuration | ||
| FLASK_SECRET_KEY=dev-secret-key-unsafe |
There was a problem hiding this comment.
Security: Unsafe default for production
The example includes FLASK_SECRET_KEY=dev-secret-key-unsafe which could accidentally be used in production.
Recommendation:
# Flask configuration
# WARNING: Generate a secure random key for production!
# python -c 'import secrets; print(secrets.token_hex(32))'
FLASK_SECRET_KEY=dev-secret-key-CHANGE-THIS-IN-PRODUCTIONConsider adding a startup check in the Flask app that errors if the secret key matches known insecure defaults.
Include reasoning in LangSmith experiment data upload Move 'facts' from input to reference_output in LangSmith dataset creation so that the LLM-as-a-Judge has that for comparison
Implements issue codeforpdx#253 with complete Docker support: - Multi-stage Dockerfiles for backend and frontend (dev/deploy types) - docker-compose.yml for local development with hot reload - docker-compose.prod.yml for production deployment - Documentation updates in README, Architecture, and CLAUDE files Key features: - Dev images include all tooling, tests, and hot reload - Deploy images are minimal with production-only dependencies - Support for Mac, Linux, and Windows development environments Note: GitHub Actions workflow file (.github/workflows/docker-build.yml) needs to be added separately by a maintainer with workflow permissions. Co-authored-by: yangm2 <yangm2@users.noreply.github.com>
only install `make` in dev use uv's Docker best practices i.e. caching
dd78738 to
b08a931
Compare
This comment was marked as outdated.
This comment was marked as outdated.
| - MODEL_NAME=${MODEL_NAME:-gemini-2.5-pro} | ||
| - LOG_LEVEL=${LOG_LEVEL:-DEBUG} | ||
| - LANGSMITH_API_KEY=${LANGSMITH_API_KEY} | ||
| - LANGSMITH_TRACING=${LANGSMITH_TRACING:-true} |
There was a problem hiding this comment.
LangSmith tracing defaults to true, meaning every developer who runs docker compose up without a .env file will send traces (and incur API costs) unless they explicitly opt out. Consider defaulting to false:
| - LANGSMITH_TRACING=${LANGSMITH_TRACING:-true} | |
| - LANGSMITH_TRACING=${LANGSMITH_TRACING:-false} |
| - LOG_LEVEL=${LOG_LEVEL:-DEBUG} | ||
| - LANGSMITH_API_KEY=${LANGSMITH_API_KEY} | ||
| - LANGSMITH_TRACING=${LANGSMITH_TRACING:-true} | ||
| - LANGCHAIN_TRACING_V2=${LANGCHAIN_TRACING_V2:-true} |
There was a problem hiding this comment.
Same opt-out concern as LANGSMITH_TRACING above.
| - LANGCHAIN_TRACING_V2=${LANGCHAIN_TRACING_V2:-true} | |
| - LANGCHAIN_TRACING_V2=${LANGCHAIN_TRACING_V2:-false} |
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
| - ./backend/tests:/app/tests:ro | ||
| - ./backend/scripts:/app/scripts:ro | ||
| # Mount Google credentials from host | ||
| - ${GOOGLE_APPLICATION_CREDENTIALS}:/app/secrets/google-creds.json:ro |
There was a problem hiding this comment.
The variable here is ${GOOGLE_APPLICATION_CREDENTIALS}, but .env.docker.example defines GOOGLE_APPLICATION_CREDENTIALS_HOST. These names don't match, so a developer who copies the example file to .env will have an unset variable — Docker will then create a directory at the target path instead of mounting the file (the root cause of the "directory instead of file" issue mentioned in the PR description).
| - ${GOOGLE_APPLICATION_CREDENTIALS}:/app/secrets/google-creds.json:ro | |
| - ${GOOGLE_APPLICATION_CREDENTIALS_HOST}:/app/secrets/google-creds.json:ro |
| - "3000:5000" | ||
| volumes: | ||
| # Mount source for hot reload | ||
| - ./backend/tenantfirstaid:/app/tenantfirstaid:ro |
There was a problem hiding this comment.
Read-only mounts here prevent make fmt (ruff format) and make lint --fix from modifying files in-place. But CLAUDE.md documents exactly these commands as the Docker workflow:
docker compose exec backend make fmt
docker compose exec backend make lint
Running those will silently produce no changes (or fail with a permission error), which is confusing. Either drop :ro on the source mounts, or note in CLAUDE.md that formatters must be run on the host.
| - ./backend/tenantfirstaid:/app/tenantfirstaid:ro | |
| - ./backend/tenantfirstaid:/app/tenantfirstaid | |
| - ./backend/tests:/app/tests | |
| - ./backend/scripts:/app/scripts |
| - MODEL_NAME=${MODEL_NAME:-gemini-2.5-pro} | ||
| - LOG_LEVEL=${LOG_LEVEL:-DEBUG} | ||
| - LANGSMITH_API_KEY=${LANGSMITH_API_KEY} | ||
| - LANGSMITH_TRACING=${LANGSMITH_TRACING:-true} |
There was a problem hiding this comment.
LangSmith tracing defaults to opt-in (true), meaning any developer who runs docker compose up without a .env will send traces and incur API costs. Consistent with the second reviewer's note — prefer opt-out:
| - LANGSMITH_TRACING=${LANGSMITH_TRACING:-true} | |
| - LANGSMITH_TRACING=${LANGSMITH_TRACING:-false} |
| - LOG_LEVEL=${LOG_LEVEL:-DEBUG} | ||
| - LANGSMITH_API_KEY=${LANGSMITH_API_KEY} | ||
| - LANGSMITH_TRACING=${LANGSMITH_TRACING:-true} | ||
| - LANGCHAIN_TRACING_V2=${LANGCHAIN_TRACING_V2:-true} |
There was a problem hiding this comment.
| - LANGCHAIN_TRACING_V2=${LANGCHAIN_TRACING_V2:-true} | |
| - LANGCHAIN_TRACING_V2=${LANGCHAIN_TRACING_V2:-false} |
backend/Dockerfile
Outdated
|
|
||
| # Sync the project | ||
| RUN --mount=type=cache,target=/root/.cache/uv \ | ||
| uv sync --locked |
There was a problem hiding this comment.
The first uv sync on line 30 uses --frozen, but this second sync uses --locked. Both prevent lockfile writes, but --frozen is the stricter form (skips resolution entirely), matches the deploy stage, and avoids any possibility of touching the lockfile. Use --frozen for consistency:
| uv sync --locked | |
| uv sync --frozen |
| @@ -0,0 +1,46 @@ | |||
| services: | |||
| backend: | |||
| image: ghcr.io/codeforpdx/tenantfirstaid/backend:latest | |||
There was a problem hiding this comment.
Using :latest in production means two docker compose pull calls a week apart may deploy different code. Pin to a specific tag (e.g., a git SHA or semantic version) so deployments are reproducible and rollbacks are straightforward.
.env.docker.example
Outdated
|
|
||
| # Path to Google credentials on your HOST machine | ||
| # Default location after 'gcloud auth application-default login' | ||
| GOOGLE_APPLICATION_CREDENTIALS_HOST=/home/<USERNAME>/.config/gcloud/application_default_credentials.json |
There was a problem hiding this comment.
The variable name here is GOOGLE_APPLICATION_CREDENTIALS_HOST, but docker-compose.yml reads ${GOOGLE_APPLICATION_CREDENTIALS} on line 18. One of them needs to change — see the inline comment on the compose file for the suggested fix there.
| LOG_LEVEL=DEBUG | ||
|
|
||
| # Flask configuration | ||
| FLASK_SECRET_KEY=dev-secret-key-unsafe |
There was a problem hiding this comment.
The "unsafe" suffix is easy to miss. Add an explicit generation command and make the warning harder to skip:
| FLASK_SECRET_KEY=dev-secret-key-unsafe | |
| # WARNING: Replace with a strong random value before any non-local use. | |
| # Generate one with: python -c 'import secrets; print(secrets.token_hex(32))' | |
| FLASK_SECRET_KEY=dev-secret-key-CHANGE-THIS |
| # LangSmith (optional) | ||
| LANGSMITH_API_KEY=<YOUR_LANGSMITH_KEY> | ||
| LANGSMITH_TRACING=true | ||
| LANGCHAIN_TRACING_V2=true |
There was a problem hiding this comment.
If LANGSMITH_TRACING defaults to false in the compose file (see suggestion there), the values here should match to avoid confusing developers who copy the example as-is.
| LANGCHAIN_TRACING_V2=true | |
| LANGSMITH_TRACING=false | |
| LANGCHAIN_TRACING_V2=false |
|
Claude finished @yangm2's task in 2m 30s —— View job PR #262 Review: Add Docker FlowGood overall structure — the multi-stage Dockerfile pattern is solid, and the recent commits addressed several of the earlier review concerns. Here's what's confirmed fixed vs. what still needs attention. Confirmed Fixed (recent commits)
Outstanding Issues1. LangSmith tracing defaults to Any developer who runs - LANGSMITH_TRACING=${LANGSMITH_TRACING:-false}
- LANGCHAIN_TRACING_V2=${LANGCHAIN_TRACING_V2:-false}2. Read-only volume mounts block
docker compose exec backend make fmt
docker compose exec backend make lintWith - ./backend/tenantfirstaid:/app/tenantfirstaid
- ./backend/tests:/app/tests
- ./backend/scripts:/app/scripts3. Production uses image: ghcr.io/codeforpdx/tenantfirstaid/backend:latest
image: ghcr.io/codeforpdx/tenantfirstaid/frontend:latestTwo 4. If the compose file defaults to LANGSMITH_TRACING=false
LANGCHAIN_TRACING_V2=false5.
# WARNING: Replace with a strong random value before any non-local use.
# Generate one with: python -c 'import secrets; print(secrets.token_hex(32))'
FLASK_SECRET_KEY=dev-secret-key-CHANGE-THISMinor / Design NotesGunicorn worker count is hardcoded —
No healthchecks in either Dockerfile — Containers will appear "healthy" to Docker even if the app is unresponsive. A simple Frontend nginx only serves on port 80 — In production, TLS is presumably terminated upstream (Nginx reverse proxy is documented in Architecture.md), so this is fine. Worth a comment in DocumentationThe Summary: The core infrastructure is solid. The blocking issues before merge are the LangSmith opt-out default (#1), and the read-only source mounts that contradict the documented Docker workflow (#2). The |
| - MODEL_NAME=${MODEL_NAME:-gemini-2.5-pro} | ||
| - LOG_LEVEL=${LOG_LEVEL:-DEBUG} | ||
| - LANGSMITH_API_KEY=${LANGSMITH_API_KEY} | ||
| - LANGSMITH_TRACING=${LANGSMITH_TRACING:-true} |
There was a problem hiding this comment.
LangSmith tracing defaults to true, so every developer who runs docker compose up without a .env will send traces and incur API costs. Prefer opt-in:
| - LANGSMITH_TRACING=${LANGSMITH_TRACING:-true} | |
| - LANGSMITH_TRACING=${LANGSMITH_TRACING:-false} |
| - LOG_LEVEL=${LOG_LEVEL:-DEBUG} | ||
| - LANGSMITH_API_KEY=${LANGSMITH_API_KEY} | ||
| - LANGSMITH_TRACING=${LANGSMITH_TRACING:-true} | ||
| - LANGCHAIN_TRACING_V2=${LANGCHAIN_TRACING_V2:-true} |
There was a problem hiding this comment.
Same as above — opt-out:
| - LANGCHAIN_TRACING_V2=${LANGCHAIN_TRACING_V2:-true} | |
| - LANGCHAIN_TRACING_V2=${LANGCHAIN_TRACING_V2:-false} |
| - ./backend/tenantfirstaid:/app/tenantfirstaid:ro | ||
| - ./backend/tests:/app/tests:ro | ||
| - ./backend/scripts:/app/scripts:ro |
There was a problem hiding this comment.
These :ro mounts prevent make fmt (ruff format) and make lint --fix from writing back to the source files. But CLAUDE.md documents docker compose exec backend make fmt as the Docker dev workflow — that command will silently do nothing with read-only mounts.
Drop :ro on the source directories; keep it only on the credentials mount below.
| - ./backend/tenantfirstaid:/app/tenantfirstaid:ro | |
| - ./backend/tests:/app/tests:ro | |
| - ./backend/scripts:/app/scripts:ro | |
| - ./backend/tenantfirstaid:/app/tenantfirstaid | |
| - ./backend/tests:/app/tests | |
| - ./backend/scripts:/app/scripts |
What type of PR is this? (check all applicable)
Description
This PR adds docker and apple/container containers to the development flow. This also opens up the way to publish docker containers on ghcr.io and deploying those containers to staging and production.
Note: Building the
backend-devimage took >15 minutes(!) on my Macbook Pro w/ M2 CPU. And the resulting image (TYPE=dev) is ~350MB. We may want to look into streamlining the dev dependencies.Disclosure: The code was initially generated with Claude coding agent 🤖.
Related Tickets & Documents
QA Instructions, Screenshots, Recordings
With the
frontendcontainer, I was able to run and pass the regression tests. Without the container, thefrontendtests fail on my Macbook Pro (presumably due to a misconfigurednpmdevelopment environment ... i.e. old version ofnpm?).with apple/containers
linttestsmake checkKnown Issue: mounting the google default application credential json results in a directory instead of the json file. This needs to be resolved for local integration testing.FIXEDwith docker/compose
linttestsmake checkAdded/updated tests?
Documentation
Architecture.mdhas been updated[optional] Are there any post deployment tasks we need to perform?