AthlEAT
Guide

Deployment Guide

Deploying to production (Docker, Kubernetes, serverless)


Overview

This guide walks you through deploying the athlete-nutrition-ai API to a production environment using Docker, Kubernetes, or a serverless platform. Because the service is stateful (it maintains athlete profiles, training calendar connections, and AI conversation history), production deployments require careful attention to environment configuration, secret management, and persistent storage. Following this guide ensures your deployment is secure, scalable, and ready to handle the full primary workflow — from account creation through subscription management.


Prerequisites

Before you begin, ensure you have the following in place:

  • Docker 24.0+ and Docker Compose 2.20+ (for container-based deployments)
  • kubectl 1.27+ and a running Kubernetes cluster 1.27+ (for Kubernetes deployments)
  • Helm 3.12+ (for Kubernetes deployments using the provided chart)
  • A serverless CLI such as AWS SAM CLI 1.90+, or the Serverless Framework 3.0+ (for serverless deployments)
  • API credentials — your NUTRITION_AI_API_KEY and NUTRITION_AI_API_SECRET, obtained from your developer dashboard
  • A PostgreSQL database 15+ for persistent storage of athlete profiles, meal plans, and shopping lists
  • A Redis instance 7.0+ for session caching and AI conversation state
  • TLS certificate for your production domain (Let's Encrypt or equivalent)
  • Outbound HTTPS access from the deployment environment to reach the AI inference backend and any connected training calendar providers
  • Basic familiarity with HTTP APIs, environment variables, and container orchestration

Installation

Docker Deployment

Step 1: Pull the official image

docker pull athletenutritionai/api:latest

Step 2: Create a .env file for secrets

Never commit this file to version control.

cat > .env << 'EOF'
NUTRITION_AI_API_KEY=your_api_key_here
NUTRITION_AI_API_SECRET=your_api_secret_here
DATABASE_URL=postgresql://user:password@db-host:5432/nutrition_ai
REDIS_URL=redis://redis-host:6379
JWT_SECRET=your_jwt_secret_here
EOF

Step 3: Run the container

docker run -d \
  --name athlete-nutrition-api \
  --env-file .env \
  -p 8080:8080 \
  athletenutritionai/api:latest

Step 4: Verify the service is healthy

curl -f http://localhost:8080/health

A 200 OK response confirms the service is running and connected to its dependencies.


Docker Compose Deployment

Step 1: Create a docker-compose.yml

version: '3.9'
services:
  api:
    image: athletenutritionai/api:latest
    ports:
      - '8080:8080'
    env_file:
      - .env
    depends_on:
      - postgres
      - redis
    restart: unless-stopped

  postgres:
    image: postgres:15
    environment:
      POSTGRES_DB: nutrition_ai
      POSTGRES_USER: ${DB_USER}
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - pg_data:/var/lib/postgresql/data
    restart: unless-stopped

  redis:
    image: redis:7
    restart: unless-stopped

volumes:
  pg_data:

Step 2: Start the stack

docker compose up -d

Step 3: Run database migrations

docker compose exec api nutrition-ai migrate

Kubernetes Deployment

Step 1: Add the Helm chart repository

helm repo add athlete-nutrition https://charts.athletenutritionai.com
helm repo update

Step 2: Create a Kubernetes secret for credentials

kubectl create secret generic nutrition-ai-secrets \
  --from-literal=api-key=your_api_key_here \
  --from-literal=api-secret=your_api_secret_here \
  --from-literal=jwt-secret=your_jwt_secret_here \
  --from-literal=database-url='postgresql://user:password@db-host:5432/nutrition_ai' \
  --from-literal=redis-url='redis://redis-host:6379'

Step 3: Install the Helm chart

helm install athlete-nutrition-api athlete-nutrition/api \
  --set existingSecret=nutrition-ai-secrets \
  --set replicaCount=3 \
  --set ingress.enabled=true \
  --set ingress.host=api.yourdomain.com

Step 4: Verify rollout

kubectl rollout status deployment/athlete-nutrition-api

Serverless Deployment (AWS Lambda)

Step 1: Install the Serverless Framework

npm install -g serverless@3

Step 2: Configure your serverless.yml

service: athlete-nutrition-ai

provider:
  name: aws
  runtime: provided.al2
  region: us-east-1
  environment:
    NUTRITION_AI_API_KEY: ${ssm:/nutrition-ai/api-key}
    NUTRITION_AI_API_SECRET: ${ssm:/nutrition-ai/api-secret}
    DATABASE_URL: ${ssm:/nutrition-ai/database-url}
    REDIS_URL: ${ssm:/nutrition-ai/redis-url}
    JWT_SECRET: ${ssm:/nutrition-ai/jwt-secret}

functions:
  api:
    handler: bootstrap
    events:
      - httpApi: '*'

Step 3: Store secrets in AWS SSM Parameter Store

aws ssm put-parameter --name /nutrition-ai/api-key \
  --value your_api_key_here --type SecureString

Repeat for each secret listed above.

Step 4: Deploy

serverless deploy --stage production

Configuration

The following environment variables control the behavior of the athlete-nutrition-ai API. All variables marked required must be set before the service will start.

VariableRequiredDefaultValid ValuesEffect
NUTRITION_AI_API_KEYStringAuthenticates your instance with the AI inference backend.
NUTRITION_AI_API_SECRETStringUsed alongside the API key for HMAC request signing.
DATABASE_URLPostgreSQL connection stringSpecifies the database used to persist athlete profiles, meal plans, and shopping lists.
REDIS_URLRedis connection stringUsed to cache AI conversation state and session tokens, enabling the stateful chat advisor workflow.
JWT_SECRETString (min 32 chars)Signs and verifies authentication tokens issued during account sign-in. Use a cryptographically random value.
PORT8080Integer (1–65535)The port the HTTP server listens on.
LOG_LEVELinfodebug, info, warn, errorControls log verbosity. Use debug only in non-production environments as it may log request bodies.
AI_REQUEST_TIMEOUT_MS30000Integer (ms)Maximum time to wait for a response from the AI inference backend. Increase this if you see timeout errors during complex meal plan generation.
MAX_CONCURRENT_AI_REQUESTS10IntegerLimits parallel calls to the AI backend per instance, preventing rate-limit errors under high load.
SHOPPING_LIST_CACHE_TTL_SECONDS3600IntegerHow long generated shopping lists are cached in Redis before being regenerated. Set lower if athletes frequently update preferences.
CALENDAR_SYNC_INTERVAL_SECONDS900IntegerHow often the service polls connected training calendars for upcoming events.
ALLOWED_ORIGINS*Comma-separated URLsCORS allowed origins. Always restrict this in production to your actual client domains.
TLS_CERT_PATHFile pathPath to a TLS certificate file. Required if you are terminating TLS at the application layer rather than at a load balancer or ingress.
TLS_KEY_PATHFile pathPath to the corresponding TLS private key.

Security note: Never set LOG_LEVEL=debug in production. Debug logs may include request payloads that contain athlete dietary data and training information subject to privacy regulations.


Usage

Once deployed, all interaction with athlete-nutrition-ai happens through authenticated HTTP requests. The typical production workflow follows this sequence:

1. Create an account and sign in

Before calling any protected endpoint, create an account and exchange credentials for a JWT bearer token:

# Create account
curl -X POST https://api.yourdomain.com/v1/accounts \
  -H 'Content-Type: application/json' \
  -d '{"email": "athlete@example.com", "password": "securepassword"}'

# Sign in to receive a token
curl -X POST https://api.yourdomain.com/v1/auth/token \
  -H 'Content-Type: application/json' \
  -d '{"email": "athlete@example.com", "password": "securepassword"}'

Include the returned token as a Bearer token in the Authorization header for all subsequent requests.

2. Set up an athlete profile

curl -X POST https://api.yourdomain.com/v1/athletes \
  -H 'Authorization: Bearer <token>' \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "Jane Doe",
    "sport": "triathlon",
    "dietary_preferences": ["gluten-free"],
    "goals": ["endurance", "weight_maintenance"]
  }'

3. Connect a training calendar

curl -X POST https://api.yourdomain.com/v1/athletes/{athlete_id}/calendars \
  -H 'Authorization: Bearer <token>' \
  -H 'Content-Type: application/json' \
  -d '{"provider": "google_calendar", "oauth_token": "<oauth_token>"}'

Once connected, the service polls the calendar on the interval configured by CALENDAR_SYNC_INTERVAL_SECONDS.

4. Chat with the AI nutrition advisor

curl -X POST https://api.yourdomain.com/v1/athletes/{athlete_id}/advisor/chat \
  -H 'Authorization: Bearer <token>' \
  -H 'Content-Type: application/json' \
  -d '{"message": "What should I eat the night before my long run on Saturday?"}'

5. Generate a shopping list

curl -X POST https://api.yourdomain.com/v1/athletes/{athlete_id}/shopping-lists \
  -H 'Authorization: Bearer <token>' \
  -H 'Content-Type: application/json' \
  -d '{"week_start": "2025-01-27"}'

Rate limits and retries

In production, the API enforces per-token rate limits. When you receive a 429 Too Many Requests response, inspect the Retry-After header and back off accordingly before retrying.


Examples

Example 1: Full health check against a live deployment

Verify your deployment is running and all dependencies are reachable.

curl -s https://api.yourdomain.com/health | jq .

Expected output:

{
  "status": "ok",
  "database": "connected",
  "redis": "connected",
  "ai_backend": "reachable",
  "version": "1.0.0"
}

If any dependency shows "unreachable" or "disconnected", consult the Troubleshooting section below.


Example 2: Creating an athlete profile and immediately requesting a meal plan

This example chains two requests — create a profile, then request a meal plan for the coming week based on synced training events.

# Step 1: Create the athlete
ATHLETE_ID=$(curl -s -X POST https://api.yourdomain.com/v1/athletes \
  -H 'Authorization: Bearer <token>' \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "Marcus Webb",
    "sport": "cycling",
    "dietary_preferences": ["vegetarian"],
    "goals": ["performance"]
  }' | jq -r '.id')

echo "Created athlete: $ATHLETE_ID"

# Step 2: Generate a meal plan
curl -s -X POST https://api.yourdomain.com/v1/athletes/${ATHLETE_ID}/meal-plans \
  -H 'Authorization: Bearer <token>' \
  -H 'Content-Type: application/json' \
  -d '{"week_start": "2025-01-27"}' | jq .

Expected output (abbreviated):

{
  "id": "mpl_abc123",
  "athlete_id": "ath_xyz789",
  "week_start": "2025-01-27",
  "days": [
    {
      "date": "2025-01-27",
      "training_event": "4h endurance ride",
      "meals": {
        "breakfast": "Oat porridge with banana and almond butter",
        "lunch": "Lentil soup with wholegrain bread",
        "dinner": "Pasta with marinara and roasted vegetables",
        "snacks": ["Energy bar", "Greek yogurt"]
      },
      "total_calories": 3200,
      "macros": {"carbs_g": 480, "protein_g": 110, "fat_g": 85}
    }
  ]
}

Example 3: Generating and retrieving a shopping list

# Generate
LIST_ID=$(curl -s -X POST https://api.yourdomain.com/v1/athletes/${ATHLETE_ID}/shopping-lists \
  -H 'Authorization: Bearer <token>' \
  -H 'Content-Type: application/json' \
  -d '{"week_start": "2025-01-27"}' | jq -r '.id')

# Retrieve
curl -s https://api.yourdomain.com/v1/athletes/${ATHLETE_ID}/shopping-lists/${LIST_ID} \
  -H 'Authorization: Bearer <token>' | jq .

Expected output (abbreviated):

{
  "id": "shl_def456",
  "week_start": "2025-01-27",
  "items": [
    {"name": "Rolled oats", "quantity": "1kg", "category": "Grains"},
    {"name": "Bananas", "quantity": "6", "category": "Fruit"},
    {"name": "Lentils", "quantity": "500g", "category": "Legumes"}
  ],
  "generated_at": "2025-01-26T10:00:00Z"
}

Example 4: Kubernetes readiness probe configuration

Configure Kubernetes to use the /health endpoint so unhealthy pods are removed from the load balancer automatically.

readinessProbe:
  httpGet:
    path: /health
    port: 8080
  initialDelaySeconds: 10
  periodSeconds: 15
  failureThreshold: 3

livenessProbe:
  httpGet:
    path: /health
    port: 8080
  initialDelaySeconds: 30
  periodSeconds: 30
  failureThreshold: 5

This ensures traffic is only routed to pods with live database and Redis connections.


Troubleshooting

Use the consistent format below — Symptom → Likely Cause → Fix — for each issue.


Symptom: The container exits immediately after starting with exit code 1 and logs show missing required environment variable.

Likely cause: One or more required environment variables (NUTRITION_AI_API_KEY, NUTRITION_AI_API_SECRET, DATABASE_URL, REDIS_URL, or JWT_SECRET) are not set.

Fix: Verify your .env file or Kubernetes secret contains all required variables listed in the Configuration section. Re-run docker compose up -d or re-apply your secret with kubectl apply.


Symptom: The /health endpoint returns 200 OK but "database": "disconnected".

Likely cause: The DATABASE_URL is incorrect, the PostgreSQL host is unreachable from the deployment network, or the database credentials are wrong.

Fix: Confirm the database is running and accessible from within the container network (e.g., docker compose exec api nc -zv db-host 5432). Double-check the username, password, and database name in DATABASE_URL.


Symptom: Requests to /v1/athletes/{id}/advisor/chat intermittently return 504 Gateway Timeout.

Likely cause: The AI_REQUEST_TIMEOUT_MS value is too low for complex meal plan queries, or the AI inference backend is under load.

Fix: Increase AI_REQUEST_TIMEOUT_MS (e.g., to 60000) and redeploy. Also check MAX_CONCURRENT_AI_REQUESTS — if it is too high, your instance may be hitting upstream rate limits, which paradoxically increases latency.


Symptom: All authenticated requests return 401 Unauthorized with the message invalid signature.

Likely cause: The JWT_SECRET used to sign tokens at sign-in time differs from the secret currently loaded by the API (common after a re-deployment that rotated secrets).

Fix: Ensure all running instances share the same JWT_SECRET. After rotating the secret, all existing tokens are invalidated — clients must re-authenticate. Use a Kubernetes secret or a secret manager to ensure consistency across replicas.


Symptom: Training calendar events are not reflected in newly generated meal plans.

Likely cause: The calendar sync has not run yet, or the CALENDAR_SYNC_INTERVAL_SECONDS interval is too long for your use case. Alternatively, the OAuth token for the connected calendar may have expired.

Fix: Trigger a manual sync via POST /v1/athletes/{id}/calendars/{calendar_id}/sync. If the response returns a 401 from the calendar provider, the athlete's OAuth token has expired and they must reconnect the calendar. Consider reducing CALENDAR_SYNC_INTERVAL_SECONDS for time-sensitive deployments.


Symptom: Kubernetes pods are failing readiness checks and being cycled, leading to degraded availability.

Likely cause: The initialDelaySeconds on the readiness probe is too short and the pod is being marked unready before it completes its database migration check on startup.

Fix: Increase initialDelaySeconds to at least 20 seconds. Ensure database migrations have run (nutrition-ai migrate) before scaling up new replicas — running migrations inside an init container is the recommended Kubernetes pattern.


Symptom: Serverless deployment returns 502 Bad Gateway for all requests.

Likely cause: The Lambda function cannot reach the PostgreSQL or Redis endpoints because they are inside a VPC that the function is not configured to access.

Fix: Configure the Lambda function's VPC settings (vpc.securityGroupIds and vpc.subnetIds in serverless.yml) to place the function inside the same VPC as your database and Redis instance. Ensure the security groups allow inbound connections from the Lambda subnets on the appropriate ports (5432 for PostgreSQL, 6379 for Redis).


Symptom: 429 Too Many Requests responses occur under moderate load.

Likely cause: Per-token rate limits are being hit, or MAX_CONCURRENT_AI_REQUESTS is set too low relative to your traffic volume.

Fix: Implement exponential backoff with jitter in your client, honoring the Retry-After response header. For sustained high-traffic workloads, review your subscription tier to confirm it supports your expected request volume.