Skip to content

Release: unified Docker setup, EC2 deploy pipeline, optional passkeys - #4

Merged
nexusaicodes merged 4 commits into
mainfrom
dev
Jul 12, 2026
Merged

Release: unified Docker setup, EC2 deploy pipeline, optional passkeys#4
nexusaicodes merged 4 commits into
mainfrom
dev

Conversation

@nexusaicodes

Copy link
Copy Markdown
Owner

Promotes dev to main, shipping three commits. Merging this triggers the production deploy (deploy.yml runs on push to main → builds the image → GHCR → SSM → EC2).

What ships

Commit Summary
c265a3f2 Unify the dev and prod Docker setup (#3) — one multi-stage Dockerfile (dev + production targets), one shared entrypoint, one pre-boot db:prepare lifecycle across dev and prod
452d9ce3 Add the EC2 deploy pipeline — production Dockerfile, docker-compose.prod.yml (app behind Caddy for auto-HTTPS), GitHub Actions → GHCR → SSM
a32bdf2b Make passkeys optional — owner password is the standing login; passkeys layer on top, never required

Deploy implications (read before merging)

  • This is the first run of the deploy pipeline on main — it has never executed against the box before. It will only succeed if the production GitHub Environment secrets/variables and the EC2 box are already set up per DEPLOY.md (AWS_ROLE_ARN, EC2_INSTANCE_ID, APP_HOST, SECRET_KEY_BASE, MUDDA_OWNER_*, box + EBS + SSM agent).
  • The production runtime is unchanged by the Docker-unification work: the production image stage is behavior-identical to before (same size, same build steps); we only added an explicit target: production and refactored dev-only tooling.

Verification already done on dev

  • Real builds (not just lint): docker build --target production and default docker build → both 491 MB (default == production); dev image → 1.15 GB. All exit 0.
  • prod image runs as uid=1000, boots Rails in production; dev image boots in development.
  • make fresh: db:prepare (incl. seeding) runs in an ephemeral container before the app container starts; app then boots healthy (200 on /up).

🤖 Generated with Claude Code

nexusaicodes and others added 4 commits July 5, 2026 00:27
Password sign-in (MUDDA_OWNER_PASSWORD) now works whenever the secret is
configured, regardless of passkeys. Passkeys become an optional convenience
for biometric/device sign-in rather than a required step.

- Rename BootstrapPassword -> OwnerPassword; enabled? is now just configured?
- Remove the PasswordLockdown middleware, the forced PasskeyEnrollment concern,
  the welcome-letter flow, and the "can't delete your only passkey" guard
- Sign-in page shows the password form and, when passkeys exist, a passkey button
- Reframe seeds, auth:reset, and docs; the seed still warns when there is neither
  a password secret nor a passkey (no way to sign in)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Deploys to a single EC2 box over Docker. GitHub Actions builds the production
image, pushes to GHCR, and deploys via AWS SSM (OIDC — no static AWS keys, no
SSH). Caddy terminates TLS with auto-HTTPS for an *.sslip.io name, so no DNS is
required and passkeys work again over HTTPS.

- Dockerfile: multi-stage production image (assets precompiled, runs non-root)
- docker-compose.prod.yml + deploy/Caddyfile: app behind Caddy, EBS-backed
  /rails/storage, only 80/443 published
- deploy/remote-deploy.sh: render runtime .env, pull from GHCR, db:prepare, up -d
- .github/workflows/deploy.yml: build+push GHCR, OIDC to AWS, ssm send-command
- DEPLOY.md: box prep, GitHub Environment secrets/vars, backups, rollback
- Docs-sync: AGENTS.md/CLAUDE.md no longer claim "no deploy tooling"

App secrets come from GitHub Actions secrets injected at deploy time (never at
build); they transit SSM command history — documented, with an SSM Parameter
Store upgrade path noted.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ycle) (#3)

* Unify dev and prod Dockerfiles into one multi-stage build

Replace the separate Dockerfile.dev with a single multi-stage Dockerfile
whose stages share one base, so the Ruby version and bundler path can't
drift between environments:

  base -> dev                              (development target)
  base -> prod-base -> build -> production (default target)

- docker-compose.yml builds `target: dev`.
- deploy.yml builds `target: production` explicitly (it's also the last
  stage, so a plain `docker build` still yields the prod image).
- Sync DOCKER.md, DEPLOY.md, AGENTS.md, CLAUDE.md to the named targets.

Both targets pass `docker buildx build --check` with no warnings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Unify the dev and prod container entrypoint and DB lifecycle

Dev now prepares the database as an explicit pre-boot step: `make
setup`/`make fresh` run `db:prepare` in a throwaway container before
`up -d`, exactly like the production deploy (deploy/remote-deploy.sh).
With migration no longer happening on boot, dev and prod share one dumb
entrypoint that only clears a stale Puma pidfile.

- Delete bin/docker-entrypoint-dev; the dev image now uses the same
  /rails/bin/docker-entrypoint as production.
- Makefile: add a `prepare` target; setup/fresh do build -> prepare -> up -d.
- docker-compose.yml: drop the redundant `command` override so dev uses
  the image CMD, like prod.
- Dockerfile: dev stage points at the shared entrypoint; align its CMD.
- DOCKER.md: document the shared entrypoint and the pre-boot prepare.

Behavior change: a failed db:prepare (e.g. a broken db/seeds.rb) now
aborts `make setup`/`fresh` instead of warning and booting without
seeds — matching the production deploy's fail-fast.

Verified end to end: `make fresh` runs db:prepare (with seeding) in an
ephemeral container before mudda-web-1 starts, then the app boots
healthy (200 on /up).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The box is a generic VPS (not EC2), so the SSM Run Command transport no longer
applies. Swap only the delivery mechanism — the image build, compose stack,
Caddy TLS, and on-box deploy script are provider-neutral and unchanged.

Deploy job now renders the runtime .env in the runner, scp's the compose file,
Caddyfile, and .env to /srv/mudda over the encrypted SSH channel, then runs
remote-deploy.sh. Secrets travel via printf %s (no shell expansion) into a 0600
file, never on a command line. The GHCR image is public, so remote-deploy.sh
drops docker login and base64 and just pulls, migrates, and rolls the stack.

The deploy user (saksham) owns /srv/mudda and is in the docker group, so no step
needs sudo; its uid 1000 matches the container's rails user, so storage perms
line up without a chown. Host key is pinned via SSH_KNOWN_HOSTS.

Docs (DEPLOY.md, AGENTS.md, CLAUDE.md, compose/Caddy comments) updated to match.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@nexusaicodes
nexusaicodes merged commit 09ba54a into main Jul 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant