Skip to content

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:

  1. Resolve the database URL and run appmecca.db.migrate.ensure_database(url) — the same alembic chain the agent runs.
  2. Fail closed: if migrations reported a fatal error, or alembic claimed head but the schema is absent, the container exits non-zero so blue/green never cuts traffic to an API that would 500 on every query.
  3. Seed status_checks from the bundled status-checks.yaml (best-effort, with {env} substituted).
  4. 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:

https://api.{env}.appmecca.net/{path}

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:

https://api.dev.appmecca.net/api/health
https://api.dev.appmecca.net/api/apps

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:

appmecca publish <ref> -e dev

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.