Skip to content

feat(docker): publish container image to ghcr.io/esnunes/destila - #148

Open
esnunes wants to merge 6 commits into
mainfrom
0c4809b1-e9e8-482f-9cbe-a1c7e493ea66
Open

feat(docker): publish container image to ghcr.io/esnunes/destila#148
esnunes wants to merge 6 commits into
mainfrom
0c4809b1-e9e8-482f-9cbe-a1c7e493ea66

Conversation

@esnunes

@esnunes esnunes commented Apr 24, 2026

Copy link
Copy Markdown
Owner

Summary

  • Ship an official Destila container image to ghcr.io/esnunes/destila via a multi-stage Dockerfile that compiles a production mix release and ships it alongside every required CLI (claude, tmux, ffmpeg, agent-browser, git) plus Chromium for agent-browser.
  • Publish on every main push and v*.*.* tag via .github/workflows/docker-publish.yml using docker/build-push-action@v5 with semver + latest + short-sha tags and GHA build cache.
  • Add a Phoenix release scaffold — lib/destila/release.ex, rel/overlays/bin/entrypoint.sh (runs migrations then bin/destila start under tini), and migrate overlays. SQLite DB lives on the /data volume (DATABASE_PATH=/data/destila.db); /root/.claude and /root/.cache/destila are also declared VOLUMEs so workflow worktrees and Claude login survive recreation.
  • Document the Docker workflow in README.md — pull, auth options, volume mounts, limitations (linux/amd64 only, root by default, dynamic service ports), and troubleshooting.
  • Couple Destila.Deps.@required_tools to the Dockerfile runtime stage via a comment so adding a tool doesn't silently break the image.

Review

ce:review (headless, 8 reviewers) returned no P0/P1 findings. Applied 4 P2 autofixes in af69c65:

  • Dropped unused id-token: write from the workflow (no OIDC signing).
  • Deleted dead rel/overlays/bin/server{,.bat} — the entrypoint calls bin/destila start directly.
  • Softened the --user $(id -u):$(id -g) recommendation in the README to flag volume-ownership EACCES risk.
  • Added the @required_tools ↔ Dockerfile coupling note.

Verification

  • mix precommit — 739 tests, 0 failures; --warnings-as-errors clean; format clean.
  • mix release produces a working release bundle locally.

Post-Deploy Monitoring & Validation

  • After merge, watch the Publish Docker image workflow in Actions → confirm the main push produces both ghcr.io/esnunes/destila:main and ghcr.io/esnunes/destila:latest.
  • Pull the image on a clean host, follow README steps 1–4, verify http://localhost:4000 loads and Destila.Deps.check/0 reports available?: true for all tools.
  • First release tag: push a v0.1.0 tag and confirm semver tags (0.1.0, 0.1, 0) land on GHCR.
  • Rollback trigger: any runtime CLI reports available?: false in production, or migrations fail on boot. Mitigation: revert to the prior image tag.

Test plan

  • Publish Docker image workflow succeeds on the first main push after merge.
  • docker pull ghcr.io/esnunes/destila:latest works from a logged-out machine (public image visibility).
  • docker run per README step 4 boots, migrates, and serves http://localhost:4000.
  • Semver tag push (v0.1.0) produces the expected 0.1.0, 0.1, 0, and latest tags.

🤖 Generated with Claude Code

esnunes and others added 6 commits April 24, 2026 16:50
Lays out a six-unit plan for shipping an official Destila container image to
ghcr.io/esnunes/destila: release scaffold via mix phx.gen.release, a multi-stage
Dockerfile that bundles claude/tmux/ffmpeg/agent-browser/git, a GHCR publish
workflow, a README "Run with Docker" section documenting ~/.claude and
~/.cache/destila mounts, and a clean-machine smoke test.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add a multi-stage Dockerfile that builds a mix release and layers every
CLI Destila.Deps checks for (claude, tmux, ffmpeg, agent-browser, git)
onto a debian-bookworm-slim runtime. Chromium is installed alongside
agent-browser so headless browser skills work out of the box.

The container auto-migrates on boot via Destila.Release.migrate/0 and
supports three bind mounts: ~/.claude for Claude CLI state,
~/.cache/destila for per-project clones and worktrees, and /data for
the SQLite DB. A new .github/workflows/docker-publish.yml pushes the
image to GHCR on main pushes and v*.*.* tags with GHA layer caching.

README gains a "Run with Docker" section that walks through pull,
SECRET_KEY_BASE generation, the three auth options, the canonical
docker run invocation, volumes reference, and troubleshooting.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Remove dead rel/overlays/bin/server{,.bat} — the entrypoint calls
  bin/destila start directly, never through these overlays.
- Drop unused id-token: write from the GHCR publish workflow; we do
  not sign or attest with OIDC.
- Note the Dockerfile coupling at Destila.Deps.@required_tools so
  future additions do not silently break the container image.
- Soften the --user docker run recommendation to flag volume-ownership
  EACCES risk instead of claiming it works out of the box.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add docker/setup-qemu-action to register arm64 emulation, then build
and push for both linux/amd64 and linux/arm64. The resulting GHCR tag
is a multi-arch manifest, so Apple Silicon and arm64 servers pull a
native image without docker run --platform overrides.

Update the README Limitations subsection to reflect that ARM hosts
are now first-class.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Switch both Dockerfile stages from the hexpm/elixir base image to plain
debian:bookworm-slim and let mise resolve erlang, elixir, and node from
mise.toml — the same source of truth used in dev. This also installs
ffmpeg and tmux from mise in the runtime stage instead of apt.

What stays apt:
  - chromium (no mise plugin)
  - ncurses/libstdc++/openssl (BEAM links to them at runtime)
  - tini (PID 1), locales (system), ca-certificates, curl, git
  - build-essential / autoconf / m4 / unzip / libssl-dev / libncurses-dev
    in the build stage so kerl can compile OTP

What moves to mise:
  - erlang, elixir (was: hexpm/elixir base image)
  - node (was: apt nodejs/npm)
  - ffmpeg, tmux (was: apt)

Tradeoff: building OTP from source via kerl adds ~5–10 minutes per arch
to the CI build relative to the pre-built hexpm image. GHA build cache
amortizes this across pushes that don't change mise.toml. Single source
of truth wins over CI runtime here.

Also tighten mise.toml syntax (drop the redundant `{ version = "..." }`
wrapper for erlang/elixir, add node@22) and update the Destila.Deps
coupling comment to reference the mise/apt split.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Switch the runtime from a `mix release` start script to running
`elixir --sname destila -S mix phx.server` directly against the
compiled source tree, with output redirected to
$HOME/.cache/destila/services/project-destila-main.log so the start
command matches how Destila launches its own managed services on a
host.

Build stage:
  - Drops `mix release`; adds `mix compile` + `mix assets.deploy`
    against MIX_ENV=prod.

Runtime stage:
  - Copies the compiled /app tree (lib, _build/prod, deps, priv,
    config, mix.exs/mix.lock) and /mise (erlang, elixir, node) from
    the build stage instead of unpacking a release tarball.
  - Entrypoint moved to /app/entrypoint.sh, sourced from
    docker/entrypoint.sh in the build context.

Cleanup:
  - Delete `lib/destila/release.ex`, `rel/overlays/bin/migrate`,
    and `rel/overlays/bin/migrate.bat` — all release-specific and
    no longer reachable.
  - Move `rel/overlays/bin/entrypoint.sh` to `docker/entrypoint.sh`
    so the path reflects the actual deployment vehicle.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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