From 107c33928bc0131c00a2f7722cff080a59bafbae Mon Sep 17 00:00:00 2001 From: Yuriy Kirillov Date: Sat, 22 Aug 2026 00:06:28 +0200 Subject: [PATCH 1/5] docs: note why there's no rollback mechanism Captures a design conclusion from discussion: the releases/current symlink pattern exists for deploy atomicity, not rollback. Automated rollback is deliberately out of scope, and manual rollback isn't a supported path - it can't restore per-release rendered config or a floating-tag image's historical version, and fixing forward through the normal deploy path is simpler and safer in practice. --- AGENTS.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index e822b8a..8884da2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -342,6 +342,8 @@ Deployment helpers live in this repository, entirely under `deploy/`, run only o - `.github/actions/encrypt-env/` is a local composite action for rendering `vaults/` manifests from GitHub Secrets/Variables, encrypting them for age recipients, and publishing `.sops.env` as a GitHub Release asset — vault manifests hold only env/secrets, not app selection - `.github/workflows/deploy-shared.yml` is a reusable workflow consumer repos call to run `deploy/deploy.py` from GitHub Actions over an optional Tailscale connection, without holding any deploy secrets in this repository +The `releases/{timestamp}`/`current` symlink pattern (Capistrano-style) exists for atomicity, not for rollback: a deploy either fully lands and only then switches the symlink as its last step, or fails partway and leaves `current` untouched — never a partially-applied app. There is deliberately no automated rollback, and manual rollback (point `current` at an old release directory by hand) is not a supported/maintained path — it wouldn't restore that release's rendered config templates (`apps-data/{app}/config/` isn't versioned per release) or a floating-tag image's historical version either, and in practice fixing forward through the normal deploy path is simpler and safer than reasoning about what a partial rollback actually restores. + App bundles listed in `app_refs` are release assets referenced as short refs like `/@latest` or `/@v1.2.3`, resolving to a default asset name of `flightdeck-apps.zip` unless the ref specifies an explicit `:asset-name` suffix. `@latest` is resolved through GitHub's latest release API. Every bundle must contain an `apps/` directory; both per-app directories and shared top-level files (`common.yml`, `networks.yml`, etc.) merge the same way — copy if new, fail loud on any name conflict across bundles. Each app in a target's `apps` mapping lists its own `env_refs` — release refs the same shape as app bundles, with no default asset name (every entry must specify an explicit `:asset-name` suffix, since there's no single obvious default under a per-app model). `deploy/deploy.py` downloads them still encrypted and checks for key collisions from the ciphertext (SOPS's dotenv output only encrypts values, so key names are readable without decryption) — scoped to that one app's own sources, not across apps, since each app ends up with its own separate `.env`. Decryption itself happens on the runner too, using the target's private age key (`credentials.secrets.sops_age_key`, a GitHub Secret) - the target host never holds this key and never runs `sops`. `apps` moved off the vault schema onto the target, and vaults moved from one-per-target to one-per-app, so that a vault can declare its output env var names directly (`HTTP_PORT`, not `TRAEFIK_HTTP_PORT`) without an implicit prefix-strip happening anywhere between the vault and the app's `.env`. From 079171030e930ae7354508941e1ec13e4c16be2d Mon Sep 17 00:00:00 2001 From: Yuriy Kirillov Date: Sat, 22 Aug 2026 00:10:51 +0200 Subject: [PATCH 2/5] docs: trim README - drop Security Best Practices, dedupe Troubleshooting Security Best Practices was generic boilerplate advice not specific to this project, and contradicted itself with Key Features' backup claim. Troubleshooting's three subsections all restated the same "cd into the app folder, docker compose logs" technique in slightly different words; collapsed into one section that states it once plus a flat list of the handful of commands that are actually distinct. --- README.md | 51 +++++++++------------------------------------------ 1 file changed, 9 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index 2b47ee6..10fa6d9 100644 --- a/README.md +++ b/README.md @@ -166,53 +166,20 @@ Add the app to whichever target's `apps` mapping should run it, and give it a va ## 🔍 Troubleshooting -Since there's no manual administration path, all of the following happen by SSHing into the target host directly, for debugging only: - -### Container Won't Start +There's no manual administration path, so debugging means SSHing into the target host and using Docker Compose directly from the app's own folder — no wrapper needed, `apps/{app}/` is already a complete, ready-to-run Compose project: ```bash -# Check logs -cd apps/app-name && docker compose logs -f - -# Validate docker-compose configuration -cd apps/app-name && docker compose config - -# Check network connectivity -docker network ls -docker network inspect traefik +cd apps/app-name +docker compose logs -f # tail logs +docker compose config # validate/inspect the resolved config +docker ps | grep app-name # confirm it's running ``` -### SSL Certificate Issues - -```bash -# Check Traefik logs -cd apps/traefik && docker compose logs -f - -# Verify ACME certificate file -ls -la apps-data/traefik/acme.json - -# Ensure correct permissions -chmod 600 apps-data/traefik/acme.json -``` - -### Application Not Accessible - -1. Verify app is running: `docker ps | grep app-name` -2. Check app logs: `cd apps/app-name && docker compose logs -f` -3. Check Traefik logs: `cd apps/traefik && docker compose logs -f` -4. Verify DNS resolves: `nslookup app-name.domain.com` -5. Test internal connectivity: `docker exec -it traefik wget -q --spider http://app-name` - -## 🔐 Security Best Practices +A few things that don't fit that one-liner: -1. **Change Default Credentials** - Update vault-sourced secrets and re-deploy -2. **Use Strong Passwords** - Generate with: `openssl rand -base64 32` -3. **Keep Images Updated** - every deploy runs `docker compose pull` before `up`, so all apps get their latest image on every deploy -4. **Restrict Network Access** - Use firewall rules to limit access to Traefik ports (80, 443) -5. **Enable HTTPS** - Always use HTTPS, never expose HTTP to internet -6. **Backup Data** - Regularly back up `apps-data/` (backup automation is a separate, not-yet-decided piece of tooling) -7. **Monitor Logs** - Review logs regularly for errors and unauthorized access attempts -8. **Update Dependencies** - Check for updates: `docker pull app:latest` +- **Networking**: `docker network ls` / `docker network inspect traefik` to check connectivity; `docker exec -it traefik wget -q --spider http://app-name` to test an app's reachability from inside the `traefik` network. +- **SSL**: `apps-data/traefik/acme.json` must exist and be `chmod 600`, or Traefik won't issue certificates. +- **DNS**: `nslookup app-name.domain.com` if the app resolves but isn't reachable. ## 📚 Additional Resources From cb86cbfb82f510428af8b81f4a59f72f24348ac1 Mon Sep 17 00:00:00 2001 From: Yuriy Kirillov Date: Sat, 22 Aug 2026 00:18:15 +0200 Subject: [PATCH 3/5] docs: drop README Contributing section Boilerplate with no actual contributors yet - write it when it's needed. --- README.md | 9 --------- 1 file changed, 9 deletions(-) diff --git a/README.md b/README.md index 10fa6d9..52b0dd0 100644 --- a/README.md +++ b/README.md @@ -189,15 +189,6 @@ A few things that don't fit that one-liner: - [Docker Compose Documentation](https://docs.docker.com/compose/) - [Traefik Documentation](https://doc.traefik.io/) -## 🤝 Contributing - -Contributions are welcome! To add a new application: - -1. Follow the "Adding a New Application" section -2. Verify by deploying it to a real target -3. Document any special requirements -4. Submit a pull request with the new app configuration - ## 🔄 Similar Services If you're evaluating alternatives, these projects solve a similar problem from different angles: From 0cd1d47c1e70ceaf3a0bea34575d012e80e236ef Mon Sep 17 00:00:00 2001 From: Yuriy Kirillov Date: Sat, 22 Aug 2026 00:24:36 +0200 Subject: [PATCH 4/5] docs: trim README further, auto-bump example version tags - License section: just "[MIT](LICENSE)" instead of a paraphrase. - Additional Resources: drop generic Docker/Compose/Traefik doc links, keep only the two project-specific ones. - Automated Deploy / Target Server Requirements: drop the repeated "nothing happens on the target host" sentence, said once is enough. - Drop the stale trailing metadata footer (Last Updated/Supported Compose/Status) - redundant with what's already stated precisely elsewhere, and not something anyone was keeping in sync. - Mark every example `@v1.2.3` in the GitHub Actions usage snippets with release-please's `x-release-please-version` generic-updater marker, and wire README.md into release-please-config.json's extra-files, so the example tags stay current automatically instead of going stale. --- README.md | 29 +++++++++-------------------- release-please-config.json | 9 ++++++++- 2 files changed, 17 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 52b0dd0..bcf6185 100644 --- a/README.md +++ b/README.md @@ -38,8 +38,6 @@ The deploy is push-based and runs entirely on the GitHub Actions runner: 5. Render that app's config templates (`config/*.template.*`) using the decrypted values — the same substitution `envsubst` does, run here instead of on the host. 6. Push the finished release (real `.env`, already-rendered config) to each host over SSH, switch the `current` symlink, and run `docker compose pull && docker compose up -d` per app. -Nothing decrypts or renders on the target host. The only thing it ever receives is a finished, ready-to-run Docker Compose project per app. - What gets deployed — which app bundles, which apps actually run, and which encrypted env sources feed each one — is configured declaratively per target; see "Vaults And Targets" below for the manifest format. ## 📁 Project Structure @@ -185,9 +183,6 @@ A few things that don't fit that one-liner: - [AGENTS.md](AGENTS.md) - Technical documentation for AI agents and developers - [RETIRED.md](RETIRED.md) - Apps removed from the active stack, and why -- [Docker Documentation](https://docs.docker.com/) -- [Docker Compose Documentation](https://docs.docker.com/compose/) -- [Traefik Documentation](https://doc.traefik.io/) ## 🔄 Similar Services @@ -303,12 +298,12 @@ Builds a zip archive from caller-selected paths, rejects runtime state and env f steps: - uses: actions/checkout@v7 with: - ref: v1.2.3 - - uses: rubykatzen/flightdeck/.github/actions/build-bundle@v1.2.3 + ref: v1.2.3 # x-release-please-version + - uses: rubykatzen/flightdeck/.github/actions/build-bundle@v1.2.3 # x-release-please-version with: paths: apps bundle-name: flightdeck-apps.zip - release-tag: v1.2.3 + release-tag: v1.2.3 # x-release-please-version token: ${{ secrets.GITHUB_TOKEN }} ``` @@ -324,10 +319,10 @@ A thin defaults wrapper around `build-bundle`: `paths` defaults to `apps`, `bund steps: - uses: actions/checkout@v7 with: - ref: v1.2.3 - - uses: rubykatzen/flightdeck/.github/actions/build-apps-bundle@v1.2.3 + ref: v1.2.3 # x-release-please-version + - uses: rubykatzen/flightdeck/.github/actions/build-apps-bundle@v1.2.3 # x-release-please-version with: - release-tag: v1.2.3 + release-tag: v1.2.3 # x-release-please-version token: ${{ secrets.GITHUB_TOKEN }} ``` @@ -346,7 +341,7 @@ Tailscale is optional, not a dependency of this workflow: set `tailscale-oauth-c ```yaml jobs: deploy: - uses: rubykatzen/flightdeck/.github/workflows/deploy-shared.yml@v1.2.3 + uses: rubykatzen/flightdeck/.github/workflows/deploy-shared.yml@v1.2.3 # x-release-please-version with: hosts: '["deploy@100.64.0.1", "deploy@100.64.0.2"]' # required JSON array app-refs: '["rubykatzen/flightdeck@latest"]' # required non-empty JSON array @@ -361,14 +356,8 @@ jobs: tailscale-oauth-secret: ${{ secrets.TAILSCALE_OAUTH_SECRET }} # optional, required only if tailscale-oauth-client-id is set ``` -The `@v1.2.3` pin on the `uses:` line only controls which ref runs `deploy/deploy.py` itself. `app-refs` entries are separate and don't have to match the workflow pin. +The `@v1.2.3` pin on the `uses:` line only controls which ref runs `deploy/deploy.py` itself. `app-refs` entries are separate and don't have to match the workflow pin. ## 📝 License -This project is provided as-is for self-hosted deployment. - ---- - -**Last Updated**: 2026 -**Supported Docker Compose**: >= 2.0 -**Status**: Active Development +[MIT](LICENSE) diff --git a/release-please-config.json b/release-please-config.json index 6c64ded..86dca03 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -4,6 +4,13 @@ "pull-request-header": "This pull request prepares the next release.", "pull-request-footer": "[Release Please documentation](https://github.com/googleapis/release-please#release-please)", "packages": { - ".": {} + ".": { + "extra-files": [ + { + "type": "generic", + "path": "README.md" + } + ] + } } } From 47a23cfe96c97fe67351286ee2bcd1b1ba6a31c5 Mon Sep 17 00:00:00 2001 From: Yuriy Kirillov Date: Sat, 22 Aug 2026 00:28:32 +0200 Subject: [PATCH 5/5] docs: move version markers outside the copy-pasteable code fences Inline # x-release-please-version comments would've been copied verbatim into a reader's own workflow file along with the rest of the example. Switched to release-please's block-marker form (x-release-please-start-version/-end) placed in the surrounding markdown, outside the fenced code blocks, so nothing extra ends up in copied code. Also swapped the 100.64.0.x IP placeholders for hostnames in the two examples this touches - a bare three-dot run within a wrapped block reads close enough to a version number that the generic updater could clip it. --- README.md | 32 ++++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index bcf6185..5f4a9da 100644 --- a/README.md +++ b/README.md @@ -239,8 +239,8 @@ apps: env_refs: - owner/config@latest:mainframe-rybbit.sops.env hosts: - - deploy@100.64.0.1 - - deploy@100.64.0.2 + - deploy@app1.example.com + - deploy@app2.example.com path: ~/flightdeck # optional, default shown credentials: variables: @@ -294,19 +294,23 @@ Secrets take precedence over Variables when both contain the same source key. Ev Builds a zip archive from caller-selected paths, rejects runtime state and env files, and uploads it to an existing GitHub Release. `paths` and `bundle-name` are required — this is a generic, reusable primitive (`build-apps-bundle` below is the only current caller). + + ```yaml steps: - uses: actions/checkout@v7 with: - ref: v1.2.3 # x-release-please-version - - uses: rubykatzen/flightdeck/.github/actions/build-bundle@v1.2.3 # x-release-please-version + ref: v1.2.3 + - uses: rubykatzen/flightdeck/.github/actions/build-bundle@v1.2.3 with: paths: apps bundle-name: flightdeck-apps.zip - release-tag: v1.2.3 # x-release-please-version + release-tag: v1.2.3 token: ${{ secrets.GITHUB_TOKEN }} ``` + + Requires `contents: write` permission on the calling job. --- @@ -315,17 +319,21 @@ Requires `contents: write` permission on the calling job. A thin defaults wrapper around `build-bundle`: `paths` defaults to `apps`, `bundle-name` defaults to `flightdeck-apps.zip`. The same action publishes flightdeck's own `apps/` catalog and any consumer repository's own app bundle. + + ```yaml steps: - uses: actions/checkout@v7 with: - ref: v1.2.3 # x-release-please-version - - uses: rubykatzen/flightdeck/.github/actions/build-apps-bundle@v1.2.3 # x-release-please-version + ref: v1.2.3 + - uses: rubykatzen/flightdeck/.github/actions/build-apps-bundle@v1.2.3 with: - release-tag: v1.2.3 # x-release-please-version + release-tag: v1.2.3 token: ${{ secrets.GITHUB_TOKEN }} ``` + + Requires `contents: write` permission on the calling job. `flightdeck-apps.zip` is the default asset name an `app_refs` entry resolves to when it doesn't specify an explicit `:asset-name` suffix; override `bundle-name` and use that suffix when publishing under a different filename. --- @@ -338,12 +346,14 @@ The interface is plain deploy vocabulary — callers never see `deploy.py`'s int Tailscale is optional, not a dependency of this workflow: set `tailscale-oauth-client-id` (and the matching `tailscale-oauth-secret`) to have the runner join a tailnet as an ephemeral node before deploying. Leave both unset to skip that step entirely — e.g. when the job already runs on a self-hosted runner with network access to the hosts, or reaches them some other way. + + ```yaml jobs: deploy: - uses: rubykatzen/flightdeck/.github/workflows/deploy-shared.yml@v1.2.3 # x-release-please-version + uses: rubykatzen/flightdeck/.github/workflows/deploy-shared.yml@v1.2.3 with: - hosts: '["deploy@100.64.0.1", "deploy@100.64.0.2"]' # required JSON array + hosts: '["deploy@app1.example.com", "deploy@app2.example.com"]' # required JSON array app-refs: '["rubykatzen/flightdeck@latest"]' # required non-empty JSON array apps: '{"traefik": {"env_refs": ["${{ github.repository }}@latest:mainframe-traefik.sops.env"]}}' # required non-empty JSON object # path: ~/flightdeck # optional, default shown @@ -356,6 +366,8 @@ jobs: tailscale-oauth-secret: ${{ secrets.TAILSCALE_OAUTH_SECRET }} # optional, required only if tailscale-oauth-client-id is set ``` + + The `@v1.2.3` pin on the `uses:` line only controls which ref runs `deploy/deploy.py` itself. `app-refs` entries are separate and don't have to match the workflow pin. ## 📝 License