Skip to content

Add docker flow#262

Draft
yangm2 wants to merge 13 commits intocodeforpdx:mainfrom
yangm2:add-docker-flow
Draft

Add docker flow#262
yangm2 wants to merge 13 commits intocodeforpdx:mainfrom
yangm2:add-docker-flow

Conversation

@yangm2
Copy link
Contributor

@yangm2 yangm2 commented Feb 11, 2026

What type of PR is this? (check all applicable)

  • Refactor
  • Feature
  • Bug Fix
  • Optimization
  • Documentation Update
  • Infrastructure
  • Maintenance

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-dev image 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 frontend container, I was able to run and pass the regression tests. Without the container, the frontend tests fail on my Macbook Pro (presumably due to a misconfigured npm development environment ... i.e. old version of npm?).

  • with apple/containers

    • frontend
      • run lint
      • run tests
    • backend
      • run make check
      • Known 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. FIXED
    • local integration testing
  • with docker/compose

    • frontend
      • run lint
      • run tests
    • backend
      • run make check
    • local integration testing

Added/updated tests?

  • Yes
  • No, and this is why: development environment changes
  • I need help with writing tests

Documentation

  • If this PR changes the system architecture, Architecture.md has been updated

[optional] Are there any post deployment tasks we need to perform?

@yangm2 yangm2 self-assigned this Feb 11, 2026
@yangm2 yangm2 added enhancement New feature or request Not ready infrastructure Pull requests related to infrastructure and underlying workflows labels Feb 11, 2026
@github-actions

This comment was marked as outdated.

@codeforpdx codeforpdx deleted a comment from github-actions bot Feb 15, 2026
Comment on lines +67 to +75
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"]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/scripts

For 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}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Configuration: Verify LangSmith defaults

LangSmith tracing is enabled by default in the dev environment. This could:

  1. Generate significant API usage/costs if developers aren't aware
  2. Send data to external service during local dev
  3. 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.

Comment on lines +6 to +7
ports:
- "127.0.0.1:5000:5000"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Production Configuration: Bind to localhost only

Good security practice binding to 127.0.0.1 (prevents direct external access), but this assumes:

  1. A reverse proxy (nginx, traefik) will handle external traffic
  2. 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}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_secret

Then 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Consider adding a startup check in the Flask app that errors if the secret key matches known insecure defaults.

yangm2 and others added 7 commits February 22, 2026 18:38
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
@github-actions

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}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

Suggested change
- 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}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same opt-out concern as LANGSMITH_TRACING above.

Suggested change
- LANGCHAIN_TRACING_V2=${LANGCHAIN_TRACING_V2:-true}
- LANGCHAIN_TRACING_V2=${LANGCHAIN_TRACING_V2:-false}

@github-actions

This comment was marked as outdated.

@github-actions

This comment was marked as outdated.

@github-actions

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
- ${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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
- ./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}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

Suggested change
- 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}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- LANGCHAIN_TRACING_V2=${LANGCHAIN_TRACING_V2:-true}
- LANGCHAIN_TRACING_V2=${LANGCHAIN_TRACING_V2:-false}


# Sync the project
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --locked
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

Suggested change
uv sync --locked
uv sync --frozen

@@ -0,0 +1,46 @@
services:
backend:
image: ghcr.io/codeforpdx/tenantfirstaid/backend:latest
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.


# 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "unsafe" suffix is easy to miss. Add an explicit generation command and make the warning harder to skip:

Suggested change
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
LANGCHAIN_TRACING_V2=true
LANGSMITH_TRACING=false
LANGCHAIN_TRACING_V2=false

@github-actions
Copy link
Contributor

github-actions bot commented Feb 23, 2026

Claude finished @yangm2's task in 2m 30s —— View job


PR #262 Review: Add Docker Flow

Good 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)

  • --frozen flag: all three uv sync calls now use --frozen consistently
  • curl | sh antipattern: replaced with the official ghcr.io/astral-sh/uv image
  • Google credentials variable name: GOOGLE_APPLICATION_CREDENTIALS now consistent between .env.docker.example and docker-compose.yml

Outstanding Issues

1. LangSmith tracing defaults to truedocker-compose.yml lines 30–31

Any developer who runs docker compose up without a .env will send traces and incur API costs with no warning. This should opt-out by default:

- LANGSMITH_TRACING=${LANGSMITH_TRACING:-false}
- LANGCHAIN_TRACING_V2=${LANGCHAIN_TRACING_V2:-false}

Fix this →

2. Read-only volume mounts block make fmtdocker-compose.yml lines 14–16

CLAUDE.md documents exactly these commands for Docker dev workflow:

docker compose exec backend make fmt
docker compose exec backend make lint

With :ro on the source mounts, both will silently produce no changes (ruff can't write) or fail with a permission error. The :ro flag makes sense for credential mounts but not for source code in a dev environment.

- ./backend/tenantfirstaid:/app/tenantfirstaid
- ./backend/tests:/app/tests
- ./backend/scripts:/app/scripts

Fix this →

3. Production uses :latestdocker-compose.prod.yml lines 3, 30

image: ghcr.io/codeforpdx/tenantfirstaid/backend:latest
image: ghcr.io/codeforpdx/tenantfirstaid/frontend:latest

Two docker compose pull calls a week apart can deploy different code silently. A git SHA or semantic tag makes rollbacks straightforward and deployments auditable. Architecture.md documents that main-<sha> tags are built — those should be used here instead.

4. .env.docker.example LangSmith defaults should match compose file

If the compose file defaults to false (per item 1 above), the example file should match to avoid confusing developers who copy it as-is:

LANGSMITH_TRACING=false
LANGCHAIN_TRACING_V2=false

5. FLASK_SECRET_KEY warning is easy to miss — .env.docker.example line 16

dev-secret-key-unsafe is subtle. A clearer signal with a generation command would help:

# 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

Minor / Design Notes

Gunicorn worker count is hardcodedbackend/Dockerfile line 81 uses -w 4. The conventional formula is 2 * CPU_count + 1. With 4 workers on a 2-CPU server this is fine, but a note or WEB_CONCURRENCY env var makes it easier to tune without rebuilding the image.

nginx:alpine is a floating tagfrontend/Dockerfile line 36. On every build this could pull a different patch of nginx. Pinning to a specific version (e.g. nginx:1.27-alpine) or a digest makes builds reproducible. Less critical in dev, but worth noting.

No healthchecks in either Dockerfile — Containers will appear "healthy" to Docker even if the app is unresponsive. A simple HEALTHCHECK instruction (curl/wget for backend, wget for frontend) improves visibility in production and in compose depends_on scenarios.

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 docker-compose.prod.yml clarifying the expected network topology.


Documentation

The Architecture.md and CLAUDE.md additions are well-written and accurate. The containerization section is a good addition. One small note: CLAUDE.md documents docker compose exec backend make fmt but the current :ro mounts make this non-functional — that should be resolved along with issue #2 above.


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 latest production tag (#3) is worth fixing before this is used to deploy.

- MODEL_NAME=${MODEL_NAME:-gemini-2.5-pro}
- LOG_LEVEL=${LOG_LEVEL:-DEBUG}
- LANGSMITH_API_KEY=${LANGSMITH_API_KEY}
- LANGSMITH_TRACING=${LANGSMITH_TRACING:-true}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

Suggested change
- 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}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above — opt-out:

Suggested change
- LANGCHAIN_TRACING_V2=${LANGCHAIN_TRACING_V2:-true}
- LANGCHAIN_TRACING_V2=${LANGCHAIN_TRACING_V2:-false}

Comment on lines +14 to +16
- ./backend/tenantfirstaid:/app/tenantfirstaid:ro
- ./backend/tests:/app/tests:ro
- ./backend/scripts:/app/scripts:ro
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
- ./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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request infrastructure Pull requests related to infrastructure and underlying workflows Not ready

Projects

None yet

Development

Successfully merging this pull request may close these issues.

improve local testing and deployments with docker containers

1 participant