{"id":51404771,"url":"https://github.com/qosha1/remote-compose","last_synced_at":"2026-07-04T10:01:28.659Z","repository":{"id":355896613,"uuid":"1170217963","full_name":"qosha1/remote-compose","owner":"qosha1","description":"Give your coding agent a full remote env with running docker composes with 1 command","archived":false,"fork":false,"pushed_at":"2026-06-15T22:55:19.000Z","size":11278,"stargazers_count":3,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-06-16T00:21:44.936Z","etag":null,"topics":["cicd","deployment","deployment-automation"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/qosha1.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-03-01T21:37:27.000Z","updated_at":"2026-06-15T22:55:23.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/qosha1/remote-compose","commit_stats":null,"previous_names":["qosha1/remote-compose"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/qosha1/remote-compose","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/qosha1%2Fremote-compose","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/qosha1%2Fremote-compose/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/qosha1%2Fremote-compose/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/qosha1%2Fremote-compose/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/qosha1","download_url":"https://codeload.github.com/qosha1/remote-compose/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/qosha1%2Fremote-compose/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35117336,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-04T02:00:05.987Z","response_time":113,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cicd","deployment","deployment-automation"],"created_at":"2026-07-04T10:01:27.873Z","updated_at":"2026-07-04T10:01:28.646Z","avatar_url":"https://github.com/qosha1.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cimg width=\"1672\" height=\"941\" alt=\"ChatGPT Image May 8, 2026, 11_07_11 AM\" src=\"https://github.com/user-attachments/assets/f4920e46-ee34-4824-a2d3-7debf3292898\" /\u003e\n\n\n# remote-compose\n\n**Take any `docker-compose.yml` and put it on AWS — as a fresh dev box with Claude already running inside, or as production infrastructure.**\n\nTwo flavors of one tool:\n\n### 🚀 `rc dev up` — disposable cloud dev environments with an agent inside\n\n```bash\nrc dev up alice \\\n  --repo https://github.com/owner/myapp \\\n  --compose docker-compose.yml \\\n  --gh-token \"$(gh auth token)\" --skip-permissions\n```\n\nIn ~5 minutes you get a fresh EC2 box with: docker, your repo cloned, the compose stack running, and **Claude Code pre-authenticated in a tmux session** waiting for you to attach. `rc dev attach alice` drops you into it. One box per agent or per branch. Work in parallel on isolated infra. `rc dev destroy` when done.\n\nMulti-repo deploys, env-file shipping, `gh`/`bd` pre-installed, shared SSH key + auth lifecycle — all built in.\n\n### 🏗️ `rc deploy` — generate a real terraform module from your compose\n\n```bash\nrc up --from-compose docker-compose.yml\n```\n\nReads your compose, asks for the handful of things compose can't express (CPU/memory, secrets, public hostname, EFS uid), generates a clean ECS terraform module, applies it. Then everyday verbs: `rc deploy`, `rc lifecycle migrate`, `rc db push`, `rc destroy`. The module is yours — `cd terraform/ \u0026\u0026 terraform apply` works without `rc`.\n\n\u003e **Status: alpha. Hand-tested against a real production Django stack.** Active branch: **`portable-deploy`**. Legacy v1 (SSH/Django-app) lives below for users on `main`. See [ARCHITECTURE.md](ARCHITECTURE.md) for design + validation, [AGENTS.md](AGENTS.md) for workflow.\n\n---\n\n## Why both modes share one tool\n\nSame insight, two surfaces. Compose is already the spec for \"how my services fit together.\" For dev we ship that spec to an EC2 box and start an agent inside. For production we render it to ECS terraform. You write the compose file once.\n\nMost deploy tooling makes you choose between cloud-specific knobs (ECS task defs, k8s manifests, Helm), opinionated black-box PaaS (Heroku, Fly, Render), or hand-rolled terraform (flexible but a 500-line module per service). `remote-compose` takes the other bet: your compose file is the topology. The tool's job is to add the few things compose can't express and emit something clean — terraform you own, or an EC2 box that disappears when you're done with it.\n\n---\n\n## Quick start\n\n\u003e **Bootstrapping a fresh machine?** Run\n\u003e `bash scripts/bootstrap-from-zero.sh` instead of step 1 — it installs\n\u003e terraform via the platform package manager (brew/apt/dnf), creates a\n\u003e `.venv`, runs `pip install -e \".[ecs]\"`, and verifies `rc doctor` is\n\u003e all-green. Idempotent — safe to re-run.\n\n```bash\n# 1. Install (only ECS provider ships today — k8s is roadmap)\npip install -e \".[ecs]\"\n\n# 2. In your app repo (alongside docker-compose.yml)\nrc init --from-compose docker-compose.yml   # scaffold a v2 rc.yml from your compose\n$EDITOR rc.yml                              # tweak cpu/memory/health checks\n\n# 3. Configure cloud creds (ECS example)\nexport AWS_PROFILE=myprofile\n\n# 4. One-shot: scaffold (if missing) → deploy → push secrets → ALB URL\nrc up --from-compose docker-compose.yml     # the lazy path, idempotent\n\n# 4b. Or step through it manually\nrc plan                            # show what terraform would create\nrc deploy                          # build images, terraform apply, force-rolls\nrc secrets push                    # upload .env files into AWS Secrets Manager\nrc lifecycle migrate               # run a named hook in a live container\nrc status                          # ECS service health table\nrc exec django -- /bin/bash        # interactive shell\nrc db push /tmp/local-dump.dump    # seed the deployed db from a local dump\nrc destroy --yes                   # tear it all down\n```\n\nTo verify the documented commands exist as advertised, run\n`bash scripts/test-readme-quickstart.sh` — it audits `rc --help` against\nthis section without touching AWS.\n\nEvery command is **declarative + idempotent**. Re-running `rc deploy` after\nno changes prints `no changes — infrastructure matches config`.\n\n---\n\n## First-deploy walkthrough\n\nIf you want to verify rc actually works end-to-end against real AWS\nbefore you commit to it, the repo ships a scripted acceptance trace\nthat takes a clean account → a fully-running production-shape Django +\ncelery + nginx stack → clean teardown. Single command, no aws-cli, no\nsed, no `/tmp` dance.\n\n```bash\n# Prereqs: terraform installed (or run scripts/bootstrap-from-zero.sh\n# first), an AWS profile with creds, and a Django+celery compose to\n# point at. We use start-simpli (private repo) — substitute your own\n# via the START_SIMPLI / COMPOSE_FILE / REGION / AWS_PROFILE_OVERRIDE\n# env vars at the top of the script.\n\nbash scripts/test-startsimpli-end-to-end.sh\n```\n\nWhat it does, step by step:\n\n1. **`rc destroy --all-ephemeral`** — clean slate. Removes any prior\n   ephemeral stacks from the local registry.\n2. **`rc up --from-compose docker-compose.local.yml --aws-profile X\n   --region Y --ttl 4h`** — single-command full deploy. Scaffolds an\n   rc.yml from your compose, auto-fixes nginx for ECS Cloud Map\n   (variable-based proxy_pass + VPC resolver), imports any orphan\n   Container Insights log group, runs terraform apply, builds + pushes\n   images, force-rolls services, pushes file-sourced secrets into\n   Secrets Manager, runs auto_on_deploy lifecycle hooks (e.g.\n   `python manage.py migrate --noinput`).\n3. **`rc status` polling** — waits for all services to reach\n   `health=healthy`.\n4. **Plain `curl http://\u003cALB\u003e/api/v1/health/`** — no Host: header\n   rewrite, no `https`, no `--insecure`. The patient retry loop tolerates\n   the ~60-90s window where ECS marks the task healthy but Django is\n   still finishing migrations + collectstatic + runserver. When it\n   returns 200, the body is checked for `{\"celery\":\"healthy\"}` —\n   real workers responding to ping, not \"no_workers\".\n5. **`rc destroy --yes`** — clean teardown. Removes every AWS resource\n   tagged `Project=\u003cthis-stack\u003e` and unregisters the entry from the\n   ephemeral registry.\n6. **`rc list --ephemeral`** — registry empty. No stale rows.\n\nScripted. Repeatable. The tracking bead is\n[rc-e5u.46](.beads/issues.jsonl) (`bd show rc-e5u.46`).\n\n---\n\n## What rc.yml v2 looks like\n\n```yaml\nversion: 2\nproject: my-app\ncompose_file: docker-compose.yml\nprovider: ecs\n\nprovider_config:\n  ecs:\n    cluster: my-app-prod\n    region: us-west-1\n    aws_profile: myprofile\n    vpc_cidr: 10.0.0.0/16               # CIDR for the VPC rc creates (default mode)\n    route53_zone: rctest.example.com   # override if zone != domain[-2:]\n\n    # --- Deploy into an EXISTING VPC (optional) -------------------------------\n    # By default rc creates its own VPC. Set vpc_id to deploy INTO an existing\n    # one instead — use this when the stack must share a VPC + security group\n    # with peer systems (so same-VPC SG references + Cloud Map DNS work, which\n    # cross-VPC peering can't replicate). All four keys are opt-in; omit them\n    # and behavior is unchanged.\n    #   vpc_id: vpc-0abc123                       # adopt this VPC (vpc_cidr unused)\n    #   public_subnet_ids: [subnet-a, subnet-b]   # \u003e=2 AZs, PUBLIC (IGW route) —\n    #                                             # ALB + Fargate (assign_public_ip)\n    #   private_subnet_ids: [subnet-c, subnet-d]  # optional; defaults to public\n    #   security_group_ids: [sg-mesh]             # extra SGs attached to every\n    #                                             # task (join an existing mesh)\n    # rc pre-flights the VPC + subnets against AWS before deploying. In adopt\n    # mode rc creates NO VPC/IGW/subnets/route-tables and does NOT touch the\n    # VPC's DHCP options, so cross-service discovery must use FQDNs\n    # (\u003csvc\u003e.\u003cproject\u003e.local) rather than short names.\n\n    # --- App-IAM grants on the task role (optional) ---------------------------\n    # rc emits ONE shared task role (aws_iam_role.task) for all services. By\n    # default it can only open SSM exec channels. Use `iam` to grant it the\n    # AWS access your app needs (S3 media, SQS, SES, ...) so you don't need an\n    # out-of-band reconcile script. Omit `iam` and the emitted terraform is\n    # byte-identical to before.\n    #   iam:\n    #     managed_policies:                 # attached as aws_iam_role_policy_attachment\n    #       - arn:aws:iam::aws:policy/AmazonSESFullAccess\n    #     statements:                       # one inline aws_iam_role_policy (task-app)\n    #       - sid: S3Media                  # optional; auto-named AppGrant\u003cN\u003e if omitted\n    #         actions: [s3:GetObject, s3:PutObject, s3:DeleteObject,\n    #                   s3:ListBucket, s3:GetBucketLocation]\n    #         resources: [arn:aws:s3:::my-bucket, arn:aws:s3:::my-bucket/*]\n    #       - actions: [elasticfilesystem:ClientMount, elasticfilesystem:ClientWrite]\n    #         resources: [arn:aws:elasticfilesystem:us-east-2:1234:file-system/fs-abc]\n    #         condition:                    # optional IAM Condition block\n    #           StringEquals:\n    #             elasticfilesystem:AccessPointArn: arn:aws:...:access-point/fsap-abc\n\nterraform:\n  output_dir: ./terraform/${provider}\n  backend:\n    type: s3                           # or local\n    bucket: my-app-tf-state\n    key: ecs.tfstate\n    region: us-west-1\n\n# Auto-creates the backup S3 bucket via terraform with versioning + AES256\n# + 14-day expiration. Set bucket_managed: false to point at an existing\n# bucket you own elsewhere.\nbackup:\n  bucket: my-app-backups\n  service: postgres                    # which container hosts pg_restore\n  retention_days: 14                   # or \"never\"\n\n# .env files become Secrets Manager JSON blobs; provider emits one\n# task-def `secrets[]` entry per KEY using arn:KEY:: selectors so each\n# key arrives as its own env var (vs one giant blob).\nsecrets:\n  - name: django\n    source: file\n    path: .envs/.production/.django\n  - name: postgres\n    source: file\n    path: .envs/.production/.postgres\n\n# Compose-driven deploy set. Default: every compose service deploys with\n# sensible defaults; rc.yml services[] is for overrides. Use exclude/include\n# for dev-only services (ngrok, debug profiles, etc.).\ncompose:\n  exclude: [ngrok]                     # mutually exclusive with include\n\n# CI/bootstrap IAM — the GitHub OIDC role CI assumes to trigger deploys. This is\n# NOT a per-service runtime resource, so `rc bootstrap` emits it into a COMMITTED\n# stack with its own terraform state (see \"CI bootstrap\" below). Strictly opt-in:\n# omit the whole key and nothing changes. ${project}/${cluster} interpolate.\nbootstrap:\n  github_oidc_deploy_role:\n    github_repo: my-org/my-app         # owner/repo (required)\n    github_branch: main                # exact branch (StringEquals); \"*\" = any ref\n    # role_name: my-ci-deploy          # default ${project}-github-deploy; set to\n                                        #   match a live role for import -\u003e no-op\n    # create_oidc_provider: false       # default: adopt the account-global provider\n    permissions:                       # each key -\u003e a least-privilege IAM statement\n      codebuild_project: ${project}-build\n      ecr_namespace:     ${project}/*\n      ecs_clusters:      [${cluster}, 'foundry-tenant-*']   # wildcard = StringLike\n      pass_roles:        [${project}-task, ${project}-task-exec]\n\nservices:\n  postgres:\n    type: infrastructure\n    cpu: 512\n    memory: 1024\n    # rc-7ga: exclude from the default `rc deploy` build+force-roll. A single-\n    # task EFS service rolls with min_healthy=0, so rolling it on every app\n    # deploy briefly drops its Cloud Map DNS record (dependents get [Errno -2]).\n    # terraform still manages it; deploy deliberately with\n    # `rc deploy --services postgres` when its image/config actually changes.\n    auto_roll: false\n    volumes:\n      - name: pgdata\n        mount: /var/lib/postgresql/data\n        # Per-service posix_user on the EFS access point so initdb\n        # can chown — postgres:17 alpine = uid 70, debian = 999.\n        uid: 999\n        gid: 999\n        mode: \"0700\"\n\n  django:\n    type: application\n    cpu: 1024\n    memory: 2048\n    port: 8001\n    health_check_path: /api/v1/health/\n    ephemeral_storage: 21              # GiB; FARGATE 21–200\n    lifecycle:\n      migrate:\n        command: [\"python\", \"manage.py\", \"migrate\", \"--noinput\"]\n        auto_on_deploy: true           # runs after every rc deploy\n      createsuperuser:\n        command: [\"python\", \"manage.py\", \"createsuperuser\", \"--noinput\"]\n        run_once: true                 # skips when probe exits 0\n        probe:\n          - python\n          - -c\n          - |\n            import os, django, sys\n            django.setup()\n            from django.contrib.auth import get_user_model\n            sys.exit(0 if get_user_model().objects.filter(\n                email=os.environ['DJANGO_SUPERUSER_EMAIL']\n            ).exists() else 1)\n      shell:\n        command: [\"python\", \"manage.py\", \"shell\"]\n        interactive: true              # forwards a TTY\n    # Per-service env from an EXISTING Secrets Manager secret (rc-7yo). Each key\n    # is wired as its own task-def secret (valueFrom \u003carn\u003e:KEY::) on THIS\n    # service only, and the arn is added to the task-exec GetSecretValue grant.\n    # Keys are explicit (rc does not call AWS at emit time). Use this for a\n    # pre-existing multi-key secret; use top-level `secrets:` when rc should\n    # CREATE the secret from a file.\n    env_from_secret:\n      - arn: arn:aws:secretsmanager:us-east-2:123:secret:myapp/prod-env-django-AbC\n        keys: [DATABASE_URL, REDIS_URL, DJANGO_SECRET_KEY]\n\n  nginx:\n    type: proxy\n    cpu: 256\n    memory: 512\n    port: 80\n    public: true\n    default_target: true               # catches anything the host rules don't\n    domain: app.example.com            # primary; this name routes here\n    aliases:                           # extra hostnames same service answers for\n      - api.app.example.com            #   (cert SANs + R53 records, no listener rules)\n    health_check_path: /health\n```\n\nFull schema reference: [ARCHITECTURE.md § rc.yml v2 at a glance](ARCHITECTURE.md#rcyml-v2-at-a-glance).\n\n---\n\n## Feature index\n\nWhat's built and live-verified on the `portable-deploy` branch:\n\n### Provider abstraction\n\n- **`Provider` ABC** — every cloud target (ECS today, K8s next) implements `emit_terraform`, `plan`, `deploy`, `redeploy`, `status`, `logs`, `exec`, `rollback`, `destroy` against a shared `DeployContext`.\n- **`FakeProvider`** for tests — every contract test runs against both `ECSProvider` and `FakeProvider` so adding a new provider is a copy-paste exercise.\n- **rc-test-* tag** — every project named `rc-test-*` gets `Environment=rc-test` tags + `force_destroy=true` on destructive resources, so test stacks always tear down clean.\n\n### ECS provider — what terraform we generate\n\n- VPC + 2 public + 2 private subnets, IGW, security groups, default routing\n- ECS cluster with Container Insights (log group terraform-managed)\n- Per-service: ECR repo, task def, ECS service, Cloud Map service-discovery entry\n- ALB with HTTP→HTTPS redirect (when `domain` is set) + ACM cert + R53 alias records\n- EFS file system + access point per stateful volume; per-service posix uid/gid/mode\n- AWS Secrets Manager: one secret per `.env` file, JSON-blobbed, individual keys exposed via ECS `arn:KEY::` selectors\n- ECS Exec wired (task role gets `ssmmessages:*`); `enable_execute_command = true` on every service\n- ALB host-routing: per-service `domain` → ALB listener rule + per-service target group\n- Single fronting service: `aliases:` adds cert SANs + R53 records without listener rules\n- S3 backup bucket auto-created with versioning + lifecycle when `backup.bucket` is declared\n- Stateful services (any with EFS) auto-set `deployment_minimum_healthy_percent = 0` so rolling deploys can't race-corrupt postgres data\n\n### CLI\n\n| command | does |\n|---|---|\n| `rc init` | scaffold a v2 rc.yml |\n| `rc migrate --in rc.yml --out rc.v2.yml` | convert legacy v1 |\n| `rc plan` | terraform plan summary |\n| `rc bootstrap [--apply]` | emit + plan the committed GitHub-OIDC CI deploy-role stack from `bootstrap:` ([details](#ci-bootstrap--committed-deploy-role-stack)); `--apply` opt-in, never destroys |\n| `rc deploy [--no-build]` | build, push, terraform apply, force-roll, run auto_on_deploy hooks |\n| `rc destroy --yes` | terraform destroy |\n| `rc status` | ECS service health table |\n| `rc exec \u003cservice\u003e -- \u003ccmd...\u003e` | run a one-off command inside a live task; reliable stdout via sentinels; full TTY when stdin is a tty |\n| `rc lifecycle \u003chook\u003e [\u003cservice\u003e]` | run a named hook from rc.yml (resolves declarer; handles `run_once` probes) |\n| `rc secrets push [--rollout/--no-rollout]` | parse each `.env` file → upload as JSON to its SM secret → force-rolls every service |\n| `rc db backup` / `rc db restore` / `rc db list` | postgres backup round-trips through S3 (host-side presigned URLs; tasks just curl) |\n| `rc db push \u003cfile\u003e` | upload a local dump → exec `pg_restore` inside the deployed container; auto-detects format from extension (`.dump`, `.tar.gz`, `.sql`); bootstraps `curl + ca-certificates` in containers that don't ship them |\n| `rc copilot import` | migrate an AWS Copilot app to rc.yml v2 + docker-compose; supports `--env \u003cname\u003e` for per-environment overrides ([guide](#aws-copilot-migration)) |\n| `rc doctor` | preflight: terraform/docker/python/boto/AWS creds checked |\n| `rc install` | platform package-manager fix for missing deps |\n\n### CI bootstrap — committed deploy-role stack\n\nThe workload stack (`deploy/\u003cproject\u003e/terraform/`) is regenerated every run and\nusually gitignored. But the **CI deploy role** — the GitHub Actions OIDC role CI\nassumes (via `sts:AssumeRoleWithWebIdentity`) to *trigger* deploys — is not a\nper-service runtime resource. It must be tracked, and if it lives out-of-band via\nthe AWS CLI it drifts. `rc bootstrap` generates it from the rc.yml `bootstrap:`\nsection into a **separate, committed stack with its own terraform state** so it's\nversioned alongside your code.\n\n```bash\nrc bootstrap            # emit bootstrap/terraform/ + terraform init + plan\nrc bootstrap --apply    # also apply (opt-in; refuses to apply a plan that destroys)\n```\n\n- **Committed, separate state.** Emitted to `bootstrap/terraform/` (override with\n  `bootstrap.output_dir`) — *outside* the regenerated workload tree, so you commit\n  it. Its backend reuses the workload bucket/lock table but with a distinct key\n  (`\u003cproject\u003e/bootstrap.tfstate`). The stack's own `.gitignore` still excludes\n  `*.tfstate` — state is never committed, only the `.tf`.\n- **Permissions → least-privilege IAM.** Each `permissions` key derives one or more\n  IAM statements with stable SIDs; region/account land as terraform data-source\n  refs (`${data.aws_region.current.name}` / `…caller_identity…account_id`), so emit\n  makes no AWS calls and stays deterministic:\n\n  | key | grants |\n  |---|---|\n  | `codebuild_project: \u003cname\u003e` | `codebuild:StartBuild`/`BatchGetBuilds`/… on that project ARN |\n  | `ecr_namespace: \u003cns\u003e/*` | `ecr:GetAuthorizationToken` (Resource `*`) + push/pull on repos under the namespace |\n  | `ecs_clusters: [a, 'b-*']` | `ecs:UpdateService`/`DescribeServices`/… scoped to each cluster's service+cluster ARNs (a wildcard entry like `foundry-tenant-*` carries straight into the ARN → StringLike-by-ARN) + `RegisterTaskDefinition`/… on `*` |\n  | `pass_roles: [r1, r2]` | `iam:PassRole` on those role ARNs, conditioned to `ecs-tasks.amazonaws.com` |\n\n- **OIDC provider: adopt by default.** The `token.actions.githubusercontent.com`\n  provider is account-global (one per account) and CI already assumes it, so the\n  stack references it via a data source. Set `create_oidc_provider: true` to have\n  rc create it instead.\n- **Adopting a live role (import → no-op).** Set `role_name` to the live role's name\n  and import it before the first apply — the generated stack README spells out the\n  exact `terraform import aws_iam_role.deploy …` / `aws_iam_role_policy.deploy …`\n  commands. `rc bootstrap` should then plan a no-op (or a small, reviewable diff).\n\n### Compose feature support\n\n- `build:` with optional `target:` (multi-stage), `args:`, `dockerfile:` — relative dockerfile resolved against the build context (the natural compose semantic)\n- `image:` — pre-built image used verbatim, ECR push skipped\n- `command:` — overrides the container CMD\n- `environment:` (dict or list) AND `env_file:` (list or single string, paths relative to compose dir, multiple files merge in declaration order, `environment:` map wins on conflict)\n- `ports:` — when public, primary port goes to ALB target group; remaining ports become additional `containerPort`s in the task def, intra-VPC reachable via the existing tasks SG (use this for VNC, devtools, internal-only ports)\n- `volumes:` — EFS-backed when declared in rc.yml with explicit `mount:` and uid/gid\n\n### Lifecycle commands\n\nDeclarative one-off operations live in rc.yml as `services[*].lifecycle.\u003chook\u003e`:\n\n```yaml\nlifecycle:\n  migrate:\n    command: [\"python\", \"manage.py\", \"migrate\", \"--noinput\"]\n    auto_on_deploy: true        # rc deploy runs this after rollout\n  createsuperuser:\n    command: [\"python\", \"manage.py\", \"createsuperuser\", \"--noinput\"]\n    run_once: true\n    probe: [python, -c, \"import sys; sys.exit(0 if user_exists() else 1)\"]\n  shell:\n    command: [\"python\", \"manage.py\", \"shell\"]\n    interactive: true           # TTY passthrough\n```\n\n`auto_on_deploy: true` runs the hook after every successful `rc deploy`,\nin declaration order, with hook failures surfaced as warnings (not deploy\nfailures — rerun `rc lifecycle \u003chook\u003e` for full output).\n\n`run_once: true` runs the `probe:` first; non-zero exit ⇒ \"not yet\ndone\" ⇒ run the hook. Idempotent createsuperuser, fixture loading,\nschema bootstrap.\n\n### AWS Copilot migration\n\nAWS Copilot reaches **end-of-support on 2026-06-12**. Every team\nrunning on Copilot needs a path off it. `rc copilot import` is that\npath — it reads any `copilot/` directory tree (services, environments,\naddons, pipelines) and writes a working `rc.yml` v2 + `docker-compose.yml`\n+ `IMPORT_SUMMARY.md`.\n\n```bash\nrc copilot import \\\n    --from ./copilot \\\n    --out  . \\\n    --env  production \\\n    --project my-app\n```\n\n**What translates today:**\n\n| Copilot construct | rc translation |\n|---|---|\n| `Backend Service` | private rc service (no public, no ALB) |\n| `Worker Service` | rc service `type: worker` |\n| `Load Balanced Web Service` | public rc service + port + `default_target` + domain (from `http.alias`) + aliases |\n| `image.build: { context, dockerfile, target, args }` | docker-compose `build:` block (multi-stage `target` honored) |\n| `image.location` | docker-compose `image:` (Copilot's `${TAG}` interpolation preserved) |\n| `cpu`, `memory`, `count` | rc.yml `cpu`, `memory`, `replicas` |\n| `storage.volumes.\u003cn\u003e: { path, efs: {uid, gid} }` | rc.yml `volumes` with EFS access-point uid/gid |\n| `variables: { KEY: value }` | docker-compose `environment:` |\n| `secrets: { KEY: { secretsmanager: arn } }` | rc.yml `secrets:` `source: aws_sm` |\n| `environments.\u003cenv\u003e` overrides | deep-merged when `--env \u003cenv\u003e` passed |\n| `${COPILOT_ENVIRONMENT_NAME}` | resolved when `--env` is passed; left literal otherwise |\n\n**What gets flagged for review** (typed warnings grouped in `IMPORT_SUMMARY.md`):\n\n| Copilot construct | warning |\n|---|---|\n| `Request-Driven Web Service` | `UnsupportedServiceTypeWarning` — App Runner is a different runtime; best-effort translated to public ECS for review |\n| `Static Site` | `UnsupportedServiceTypeWarning` — CloudFront+S3 has no ECS analogue; emitted to `compose.exclude` so it's not silently dropped |\n| `count: { range, cpu_percentage }` | `ScalingNotSupportedWarning` — autoscaling not yet emitted; replicas pinned to range floor |\n| `count: 0` | `ScalingNotSupportedWarning` — ECS doesn't scale-to-zero; replicas=1 |\n| `exec: false` | `ExecDisabledIgnoredWarning` — provider always enables ECS Exec |\n| `network.vpc.placement: private` | `PrivateSubnetUnsupportedWarning` — public-subnet Fargate today (rc-e5u.25 tracks the NAT variant) |\n| addons CFN templates | listed in summary — translate to terraform manually (P3 backlog) |\n\nTested against [a corpus of real Copilot apps](tests/fixtures/copilot/README.md) including:\n- aws/copilot-cli e2e fixtures (canonical LBWS, app-with-domain, static-site)\n- a public external example (ShanikaEdiriweera/aws-copilot-example)\n- a 15-service production-grade app (sentinal: backend + workers + nginx + multi-env + secretsmanager refs)\n\n### Local-data seeding (`rc db push`)\n\nSpin up a test stack in a separate region, seed it with real data from a\nlocal Docker volume, validate, tear down. Repeat. The flow:\n\n```bash\ndocker exec my_postgres pg_dump -Fc -U postgres my_db \u003e /tmp/seed.dump\nrc deploy\nrc secrets push\nrc db push /tmp/seed.dump\n```\n\n`rc db push` uploads to the configured backup bucket via host-side boto3,\ngenerates a presigned GET URL, exec's a sentinel-bracketed restore script\ninside the deployed postgres container that downloads with curl (or\nbootstraps curl via apt-get when the image doesn't ship it), runs\n`pg_restore --no-owner --clean --if-exists`, and deletes the S3 staging\nobject on success.\n\n---\n\n## Mental model in 5 lines\n\n1. **Compose is the topology.** Adding a service to `docker-compose.yml` deploys it (defaults: 256 CPU / 512 MB / `application` if it has ports, `worker` otherwise).\n2. **rc.yml is the tuning.** Override CPU, memory, port, public, domain, lifecycle, secrets, volumes, EFS uid, etc. per-service.\n3. **The provider is thin.** It generates a terraform module from the merged config; you can `cd terraform/ \u0026\u0026 terraform apply` without `rc` ever again.\n4. **Secrets are JSON in SM.** Each `.env` file becomes one secret; each KEY in that file becomes a separate task-def env var via ECS JSON-key selectors.\n5. **Test stacks are disposable.** `rc-test-*` projects auto-set `force_destroy=true` on every resource; `rc destroy` tears them down clean.\n\n---\n\n## Codebase map\n\n```\nremote_compose/\n├── cli.py                           # legacy v1 commands + v2 dispatch\n├── cli_v2.py                        # v2 CLI: load_rc_yml, build_deploy_context, dispatch_if_v2\n├── config/\n│   ├── v1_schema.py                 # legacy flat schema loader\n│   ├── v2_schema.py                 # ServiceV2, RcConfigV2, ComposeConfig, BackupConfig, ...\n│   └── migrate.py                   # v1 → v2 with warnings on stateful services\n├── envfile.py                       # standalone .env parser (used by provider + rc db push + lifecycle)\n├── copilot/\n│   ├── discover.py                  # walk copilot/ → typed CopilotApp model\n│   └── translate.py                 # 5 focused translators + composer + warning types\n├── provider/\n│   ├── base.py                      # Provider ABC, ServiceSpec, DeployContext, ExecResult, ...\n│   ├── fake.py                      # in-memory provider for the contract suite\n│   └── ecs/\n│       ├── provider.py              # ECSProvider implementation\n│       ├── autosize.py              # EC2 capacity provider sizing\n│       ├── ecr_auth.py              # ECR login for image push\n│       └── templates/\n│           ├── alb.tf.j2            # ALB + listeners + per-service target groups + host rules\n│           ├── backend.tf.j2        # terraform backend\n│           ├── backup.tf.j2         # S3 backup bucket + lifecycle\n│           ├── capacity.tf.j2       # EC2 capacity provider\n│           ├── cluster.tf.j2        # ECS cluster + container-insights log group\n│           ├── domain.tf.j2         # ACM cert (with SANs) + R53 records\n│           ├── efs.tf.j2            # EFS + access points (uid/gid/mode)\n│           ├── iam.tf.j2            # task-execution + task roles + ssmmessages policy\n│           ├── network.tf.j2        # VPC, subnets, IGW, route tables\n│           ├── outputs.tf.j2        # ECR repo URLs, ALB DNS\n│           ├── providers.tf.j2      # AWS provider block\n│           ├── secrets.tf.j2        # SM secret placeholders\n│           ├── security_groups.tf.j2\n│           ├── service_discovery.tf.j2  # Cloud Map private namespace\n│           ├── services.tf.j2       # ECS task def + service per compose service\n│           └── variables.tf.j2\n├── image/\n│   ├── builder.py                   # docker build wrapper (handles relative dockerfile)\n│   └── pusher.py                    # docker push to ECR/GCR/etc.\n└── terraform/\n    ├── backend.py                   # render_backend_block\n    ├── emitter.py                   # Jinja2-based directory render\n    └── runner.py                    # subprocess wrapper for terraform CLI\n\ntests/\n├── unit/                            # per-module unit tests\n├── contract/test_provider_contract.py   # runs against ECSProvider + FakeProvider\n├── integration/test_provider_ecs_terraform.py  # invokes real `terraform validate`\n├── e2e/                             # opt-in real-AWS tests (RC_E2E=1)\n└── fixtures/golden/ecs_minimal/     # byte-for-byte expected HCL output\n\nexamples/\n├── demo-app/                        # FastAPI + worker + postgres + redis reference stack\n└── sample-app/                      # minimal hello-world\n```\n\n[ARCHITECTURE.md § Layers](ARCHITECTURE.md#layers) has the import-rule\ndiagram.\n\n---\n\n## Image builds — shared-image dedup\n\nWhen several services share one build (same context + dockerfile + target +\nbuild args — the standard Django layout where `django` and the `celery-*`\nworkers run the *same* image, differing only by `command`), rc builds and\npushes that image **once** to a single ECR repo and points the sibling task\ndefinitions at it. Without this, an N-service app pushes the same image to N\nrepos — and because ECR stores layer blobs per-repo, that's N full uploads\n(hours on a slow uplink). The repo owner is the alphabetically-first service in\nthe group; nothing to configure. Services with a unique build or a pre-built\n`image:` are unaffected.\n\n## Build \u0026 test\n\n```bash\n# Dev install\npip install -e \".[ecs]\"\npip install -r requirements/dev.txt\n\n# Fast (12s): unit + contract\npytest tests/unit/ tests/contract/\n\n# Adds: real `terraform init -backend=false \u0026\u0026 terraform validate`\npytest tests/integration/\n\n# Full opt-in real-AWS suite (~25 min, requires creds)\nRC_E2E=1 pytest -m e2e tests/e2e/\n\n# Regenerate the byte-identical golden HCL fixture\npython -m tests.unit.test_provider_ecs.test_golden --regenerate\n\n# Linters\nblack remote_compose/\nflake8 remote_compose/\n```\n\nThe contract suite is the heart of provider parity. Any new provider\nships only when `pytest tests/contract/test_provider_contract.py` is\ngreen against it.\n\n---\n\n## Roadmap / open work\n\nTracked in [beads](https://github.com/steveyegge/beads). To inspect:\n\n```bash\nbd ready                      # available work\nbd show rc-e5u                # the umbrella epic\nbd list --status=open         # everything still open\n```\n\nHigh-signal open items (as of this writing):\n\n- **Kubernetes provider** (`rc-e5u.8`) — proves the multi-cloud claim\n- **Private subnets + NAT** (`rc-e5u.25`) — currently public-subnet Fargate for cost\n- **EFS encryption on fresh accounts** (`rc-e5u.26`) — KMS key bootstrap\n- **`rc audit`** (`rc-e5u.37.4`) — post-destroy AWS-side cleanup verification\n- **`rc db dump-local`** (`rc-e5u.37.3`) — wraps `docker exec pg_dump` with port autodiscovery\n- **`rc compose import`** (`rc-e5u.41.3`) — scaffold rc.yml from a compose file\n- **Framework presets** (`rc-e5u.35.7`) — auto-default lifecycle hooks for django/rails/phoenix/laravel\n- **Provider auto-import of orphan log groups** (`rc-e5u.37.5`) — terraform import on first-run conflicts\n\n---\n\n## Design principles\n\n1. **Be a generator, not a runtime.** Every piece of state we own should also be readable as plain terraform / plain JSON. Users escape `rc` cleanly.\n2. **Compose is the contract.** Don't invent parallel config; consume the file the team already maintains.\n3. **Test against real clouds.** Unit tests catch shape regressions; the real validator is `terraform validate` + a live e2e against `rc-test-*` projects.\n4. **One-off operations get first-class commands.** Lifecycle hooks, db push, exec, secrets push — all CLI verbs, not bash scripts users have to copy.\n5. **Reproducible test stacks.** `rc-test-*` namespaces auto-tear-down; isolation is a property of the project name, not user discipline.\n6. **No backwards-compat ratchets in alpha.** When the right shape conflicts with the old shape, file a bead, change both at once. Backward-compat shims live only as long as we're sure they don't trap us.\n\nSee [AGENTS.md](AGENTS.md) for the day-to-day workflow.\n\n---\n\n## Related docs\n\n- [ARCHITECTURE.md](ARCHITECTURE.md) — full design, validation ladder, dependency graph, e2e setup\n- [AGENTS.md](AGENTS.md) — workflow conventions for humans + AI agents\n- [examples/demo-app/README.md](examples/demo-app/README.md) — runnable reference stack\n- [CLAUDE.md](CLAUDE.md) — instructions for Claude Code when working in this repo\n\n---\n\n# Legacy v1 (pre-portable)\n\nThe content below describes the v1 SSH/Django-app deploy path on `main`.\nThe portable provider work above lives on `portable-deploy`. v1 still ships\nfor users on the older path; v2 is the active line.\n\n## Features (v1)\n\n- **Docker Context Management**: Create and manage Docker contexts for remote deployment targets\n- **Docker Compose Deployment**: Deploy docker-compose.yml files to remote hosts via SSH\n- **AWS EC2 Integration**: Auto-discover EC2 instances and create deployment targets\n- **AWS ECS Integration**: Deploy to AWS ECS (Fargate or EC2) without SSH\n- **Async Deployments**: Celery tasks for background deployment operations\n- **Health Monitoring**: Continuous health checks for targets and deployments\n- **Multi-Service Orchestration**: Deploy multiple services with sequential, parallel, rolling, or canary strategies\n- **Rate Limiting**: Protect against deployment abuse with configurable rate limits\n- **Audit Logging**: Track all deployment-related actions for compliance\n- **Secure Credential Storage**: Fernet-encrypted storage for SSH keys and AWS credentials\n- **Log Sanitization**: Automatic masking of sensitive data in logs\n- **Webhooks \u0026 Notifications**: Slack, email, and custom webhook notifications\n- **Deployment History**: Full deployment tracking with rollback capability\n\nFor the full v1 reference (Django settings, management commands, API\nviewsets, etc.) see the file history of this README in `git log` —\nthe prior version is preserved at `git show main:README.md`.\n\n## License\n\nMIT — see [LICENSE](LICENSE) for terms.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fqosha1%2Fremote-compose","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fqosha1%2Fremote-compose","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fqosha1%2Fremote-compose/lists"}