Docker Deployment¶
Container¶
| Property | Value |
|---|---|
| Base image | python:3.11-slim (multi-stage build) |
| Server | uvicorn (2 worker processes) |
| Port | 8080 |
| Entry point | appmecca_api.main:app |
| Entrypoint script | entrypoint.sh — runs migrations, then execs the CMD |
| Network | Docker host private network |
The API runs as a long-lived Docker container on the AppMecca Docker host pool, deployed via the standard CodeBuild + agent pipeline (same as any other app). One image backs all four services — api, worker, beat and canary — which differ only by their command.
Build-time requirements¶
The Dockerfile takes a required PG_MAJOR build arg with no default. CodeBuildProjectHandler derives it from the environment's rds.instances[].engine_version and passes it as --build-arg, so the bundled postgresql-client major can never drift behind the live RDS server; the build fails outright if it is missing, and again if the installed pg_dump major does not match.
The build also stages the appmecca_common wheel into the build context. The API imports appmecca.* at runtime, that package is not on PyPI, and a container built without the wheel crash-loops on ModuleNotFoundError: No module named 'appmecca'.
Startup sequence¶
entrypoint.sh runs before the application starts:
- Resolve the database URL and run
appmecca.db.migrate.ensure_database(url)— the same alembic chain the agent runs. - Fail closed: if migrations reported a fatal error, or alembic claimed
headbut the schema is absent, the container exits non-zero so blue/green never cuts traffic to an API that would 500 on every query. - Seed
status_checksfrom the bundledstatus-checks.yaml(best-effort, with{env}substituted). exec "$@"— hand off to the service's command.
Connection Pool Lifecycle¶
The asyncpg connection pool is created lazily on first use and persists for the lifetime of the container, then is closed by the FastAPI lifespan hook on shutdown. This avoids per-request connection setup and gives consistent low-latency database access. It is handled automatically in db/connection.py.
Each connection registers jsonb / json type codecs, so JSONB columns decode into Python dict / list rather than raw JSON strings — response models can keep their natural type hints with no per-call-site json.loads.
The pool is per worker process
db_max_pool_size bounds one uvicorn worker, not the container. With
--workers 2 and replicas: 3, a fully-warm environment can hold up to
2 × 3 × max_pool_size connections from the api service alone — before
the worker and beat containers. Size it against the RDS
max_connections ceiling.
uvicorn Server¶
The image's CMD is:
uvicorn appmecca_api.main:app --host 0.0.0.0 --port 8080 \
--workers 2 --proxy-headers --forwarded-allow-ips '*'
The FastAPI app handles ASGI requests natively — no adapter layer is needed.
--proxy-headers with --forwarded-allow-ips '*' makes uvicorn populate request.client.host from the forwarded client IP rather than the ALB's private IP. Trusting the header is acceptable only because the sole path to the container is ALB → uvicorn inside the closed VPC — no tenant container can address the API socket directly.
A Docker HEALTHCHECK polls http://localhost:8080/api/health every 30 s, and the ALB target group health check uses the same path (see the API's appmecca.yaml).
URL Structure¶
The API is exposed via the ALB with host-based routing:
The platform API has a reserved hostname (api.{domain}) that is separate from the standard tenant pattern. The reservation lives in appmecca-common/src/appmecca/data/hostname-aliases.yaml as appmecca-api/api: api, and every hostname on the platform is built by the one canonical function appmecca.deploy.hostname.canonical_hostname.
For example:
No ?environment= parameter
Each AppMecca environment has its own database, so the redundant
environment query parameter was removed from the API surface and the
environment column was dropped from the schema (migration 010). The
container's environment comes from APPMECCA_API_ENVIRONMENT and is read
via appmecca_api.config.env().
CORS origins come from APPMECCA_API_CORS_ORIGINS (default ["*"]). When the list is the wildcard, credentialed CORS is disabled — a wildcard origin combined with allow_credentials=True makes Starlette reflect the caller's Origin, which is both a credentialed-CORS bypass and forbidden by the CORS spec. An explicit origin allow-list re-enables credentials.
IAM Permissions¶
The API container is subject to the IMDS lockdown, so it cannot use the host instance profile. IAMUserHandler creates a narrow per-environment IAM user appmecca-{env}-api-user, stores its access key at appmecca/{env}/api-aws-creds in Secrets Manager, and the agent injects the credentials as env vars at docker run time.
Its inline policy grants:
| Sid | Actions | Purpose |
|---|---|---|
SecretsManagerRead |
secretsmanager:GetSecretValue on appmecca/{env}/* |
Platform-root password bootstrap, agent-http shared secret |
S3AppBuckets |
s3:GetObject, PutObject, DeleteObject, ListBucket, GetObjectVersion |
App bucket reads/writes |
CloudWatchLogsList / CloudWatchLogsRead |
logs:DescribeLogGroups, DescribeLogStreams, FilterLogEvents, GetLogEvents on /appmecca/{env}/* |
Log streaming endpoints |
CloudFrontManage |
ListDistributions, GetDistribution(Config), CreateInvalidation, Tag/UntagResource, UpdateDistribution |
Cache management routes |
CodeBuildDescribe |
codebuild:ListProjects, BatchGetProjects |
Read build project metadata |
ELBv2Describe |
DescribeLoadBalancers, DescribeListeners, DescribeRules, DescribeTags |
The health route's load-balancer probe |
SESSendPlatformEmail |
ses:SendEmail, ses:SendRawEmail |
Platform transactional mail |
STSCallerIdentity |
sts:GetCallerIdentity |
Account validation |
What the API deliberately cannot do
The policy grants no DynamoDB, no ECR, no Route53, no
codebuild:StartBuild, and Secrets Manager is read-only. Write paths that
used to live here — ACME account keys and certificate material, app-database
credentials, ACME listener rules — all moved to the agent. Database access
is by injected password, not IAM authentication.
Deployment¶
The API is published with the rest of the platform from one git ref. The publish derives the version for the environment, builds the API via CodeBuild from that ref's commit, and redeploys it; it rebuilds only the components that changed since the environment's running release:
To pick up changed secrets without a build, appmecca api redeploy -e dev redeploys the API image that is running now.
If the API itself is unreachable — so the normal queue-write path is unavailable — appmecca api recover -e dev queues the BUILD and DEPLOY by writing directly to PostgreSQL (falling back to an SSH-exec on a Docker host when the laptop cannot reach RDS).
See Docker Deployment for configuration details.