diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..c117366 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,76 @@ +# AGENTS.md + +## Cursor Cloud specific instructions + +This repo is the **Shipyard Flask template**: a Flask + Celery app backed by +PostgreSQL, Redis, and LocalStack (emulated S3). Everything runs through Docker +Compose — there is no supported non-Docker "run locally" path. Docker itself is +already installed in the VM snapshot; the notes below cover the non-obvious +caveats for running it here. + +### Services (all defined in `docker-compose.yaml`) + +| Service | Required | Notes | +| --- | --- | --- | +| `web` | yes | Flask app on `http://localhost:8080`. With `DEV=true` it runs the Flask dev server (auto-reload); otherwise uWSGI. | +| `postgres` | yes | `web`/`worker` block on it at startup. Data lives in the `workspace_postgres` named volume. | +| `redis` | yes | Celery broker + result backend. | +| `worker` | optional | Celery worker + Beat; runs the `do_something` heartbeat every 5s (increments counters by 10). App serves pages without it. | +| `localstack` | optional | Emulated S3 for the `/files` routes only. Pinned to a free Community version (see below). | + +### Starting everything (non-obvious startup steps) + +- **The Docker daemon is NOT auto-started** (no systemd). Start it once per VM + session before any docker command, e.g. in a tmux session: + `sudo dockerd > /tmp/dockerd.log 2>&1 &` then wait a few seconds. +- **Use `sudo` for docker** (`sudo docker ...`, `sudo docker compose ...`). The + `ubuntu` user is in the `docker` group, but the group is not active in fresh + non-login shells, so the socket returns "permission denied" without sudo. +- Preserve the `DEV` env var through sudo: `DEV=true sudo -E docker compose ...`. + Set `DEV=true` for the auto-reloading dev server. +- The `Makefile` calls the v1 `docker-compose` command; only the v2 plugin is + installed, so a `/usr/local/bin/docker-compose -> docker compose` shim is used. + `make` targets still won't have docker group access, so prefer the compose + commands directly (with `sudo -E`) over `make develop`. + +Bring-up sequence (mirrors `make develop`): +``` +DEV=true sudo -E docker compose up -d postgres +sudo -E docker compose exec -T postgres sh -c 'while ! nc -z postgres 5432; do sleep 0.2; done' +DEV=true sudo -E docker compose run --rm worker poetry run flask db upgrade # migrations +DEV=true sudo -E docker compose up -d # all services +``` + +### Hello-world / smoke check + +- Counter page (increments a DB-backed counter): `curl http://localhost:8080/` +- **S3 files flow order matters**: `GET /files` creates the `test-bucket` and + lists it; `GET /files/create` uploads an object but does NOT create the bucket, + so hit `/files` first or `/files/create` returns 500 (`NoSuchBucket`). + +### Migrations & tests + +- Migrations: `... docker compose run --rm worker poetry run flask db upgrade`. + Postgres data persists in the `workspace_postgres` volume; if that volume is + reset you must re-run migrations. +- Tests: pytest is installed but the template ships **no tests** (`pytest` reports + "no tests ran"). Run with `... docker compose run --rm worker poetry run pytest`. + +### Dependency notes (already handled in the committed `Dockerfile` / compose) + +These pins exist because the template targets ancient runtimes (Python 3.8 on +Alpine 3.11) and its original unpinned dependencies no longer resolve today. If +you change `pyproject.toml`, `poetry.lock`, `Dockerfile`, or the compose file, +rebuild with `DEV=true sudo -E docker compose build` (the update script does not +rebuild images). + +- Poetry is pinned to `1.1.15` (matches `poetry.lock` lock-version 1.1 and avoids + modern Poetry's `rapidfuzz` source build on musl). +- `cryptography==2.8` is pre-installed so Poetry's keyring dep doesn't pull the + latest cryptography (needs Rust / `setuptools>=77`, impossible on py3.8). +- `poetry config virtualenvs.create false` installs into the system interpreter; + the venv Poetry seeds ships a setuptools too new to build the 2020-era sdists + (psycopg2 2.8.4, uwsgi 2.0.18, SQLAlchemy-Utils). `/home/app/.local/bin` is + added to `PATH` so `poetry run flask`/`celery` find their console scripts. +- LocalStack is pinned to `localstack/localstack:3.8.1`; the unpinned/`latest` + tag now requires a paid `LOCALSTACK_AUTH_TOKEN` and exits (code 55) without one. diff --git a/Dockerfile b/Dockerfile index 003e9f4..b558a3c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -30,7 +30,14 @@ RUn apk add --update --no-cache \ #cd .. && rm -rf watchman # Install poetry -RUN pip install poetry +# Pin to 1.1.x to match the committed poetry.lock (lock-version 1.1) and avoid +# pulling in modern Poetry's rapidfuzz dependency, which has no musllinux wheel +# for cp38 and otherwise compiles from source (very slow) on this Alpine base. +# Pre-install cryptography 2.8 (the version pinned in poetry.lock, builds against +# the openssl-dev already installed above) so that Poetry's keyring/SecretStorage +# dependency does not pull the latest cryptography, which needs setuptools>=77 and +# Rust and cannot build on this Python 3.8 image. +RUN pip install "cryptography==2.8" && pip install "poetry==1.1.15" # Create an app user, prepare permissions, and run as the user RUN adduser -S app @@ -39,6 +46,12 @@ RUN mkdir -p /var/run/celery && \ USER app # Install Python dependencies +# Install into the system interpreter instead of a Poetry-managed virtualenv. +# The virtualenv that Poetry seeds ships a newer setuptools whose +# canonicalize_version(strip_trailing_zero=...) call cannot build the 2020-era +# sdists pinned here (psycopg2 2.8.4, uwsgi 2.0.18, SQLAlchemy-Utils 0.36.3), +# whereas the base image's older system setuptools builds them fine. +RUN poetry config virtualenvs.create false ADD pyproject.toml poetry.lock ./ RUN poetry install @@ -51,6 +64,12 @@ RUN poetry install # Add system files ADD filesystem / +# With virtualenvs.create=false and a non-root user, Poetry installs packages +# (and their console scripts such as flask/celery/uwsgi) into the user site at +# /home/app/.local, which is not on PATH by default. Add it so `poetry run flask` +# and the entrypoint scripts can find these executables. +ENV PATH="/home/app/.local/bin:${PATH}" + # Set the default command ENV FLASK_APP src/entry:flask_app CMD /entrypoints/web.sh diff --git a/docker-compose.yaml b/docker-compose.yaml index 20b9ed4..d2e1075 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -51,7 +51,10 @@ services: - '6379' localstack: - image: 'localstack/localstack' + # Pinned to a free Community edition release. The unpinned/`latest` tag now + # resolves to a build that requires a paid LOCALSTACK_AUTH_TOKEN and exits + # (code 55) without one, which breaks the S3 (/files) demo flow. + image: 'localstack/localstack:3.8.1' ports: - '4566:4566' - '4571:4571'