Metadata-Version: 2.4
Name: docker-app-launcher
Version: 0.7.0
Summary: Configurable desktop launcher for Docker-based applications
License-Expression: MIT
License-File: LICENSE
Keywords: docker,launcher,desktop,tkinter,gui
Author: Asterios Raptis
Author-email: aster.raptis@gmail.com
Requires-Python: >=3.10,<4.0
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: X11 Applications
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Software Development :: Build Tools
Classifier: Topic :: System :: Installation/Setup
Classifier: Typing :: Typed
Provides-Extra: tray
Requires-Dist: Pillow (>=10.0) ; extra == "tray"
Requires-Dist: PyGObject (>=3.42,<3.52) ; (sys_platform == "linux") and (extra == "tray")
Requires-Dist: pystray (>=0.19) ; extra == "tray"
Requires-Dist: pyyaml (>=6.0)
Project-URL: Changelog, https://github.com/astrapi69/docker-app-launcher/blob/main/CHANGELOG.md
Project-URL: Documentation, https://github.com/astrapi69/docker-app-launcher#readme
Project-URL: Homepage, https://github.com/astrapi69/docker-app-launcher
Project-URL: Issue Tracker, https://github.com/astrapi69/docker-app-launcher/issues
Project-URL: Repository, https://github.com/astrapi69/docker-app-launcher
Description-Content-Type: text/markdown

# docker-app-launcher

Configurable desktop launcher for Docker-based applications.
**One persistent window.** It opens, shows progress, and never closes itself —
no dialog chains.

[![CI](https://github.com/astrapi69/docker-app-launcher/actions/workflows/ci.yml/badge.svg)](https://github.com/astrapi69/docker-app-launcher/actions/workflows/ci.yml)
[![PyPI](https://img.shields.io/pypi/v/docker-app-launcher.svg)](https://pypi.org/project/docker-app-launcher/)
[![Python](https://img.shields.io/pypi/pyversions/docker-app-launcher.svg)](https://pypi.org/project/docker-app-launcher/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/astrapi69/docker-app-launcher/blob/main/LICENSE)

> 🇩🇪 [Deutsche Version](https://github.com/astrapi69/docker-app-launcher/blob/main/README-de.md)

## Quick Start

```bash
pip install docker-app-launcher
```

### Python API (3 lines)

```python
from docker_app_launcher import LauncherConfig, launch

launch(LauncherConfig(
    app_name="My App",
    container_name="my-app",
    default_port=8080,
))
```

### CLI

```bash
docker-app-launcher --config launcher.json   # open the window
docker-app-launcher --version                 # print the launcher version and exit
docker-app-launcher --check                   # is Docker running?
docker-app-launcher --status                  # print state and exit
docker-app-launcher --install --port 9000     # build + start headless
docker-app-launcher --start                   # start the stopped app
docker-app-launcher --stop                    # stop the running app
docker-app-launcher --uninstall               # remove containers/images
docker-app-launcher --cleanup                 # remove stale leftovers
docker-app-launcher --open                    # open the app in the browser
docker-app-launcher --debug ...               # verbose logging to stdout + launcher-debug.log
```

### launcher.json

Everything is configurable. Only `app_name` is required — the rest is derived
(slug, container/image names, compose project, config dir) or defaulted.

```json
{
  "app_name": "My App",
  "container_name": "my-app",
  "default_port": 8080,
  "compose_file": "docker-compose.prod.yml",
  "install_dir": "/opt/my-app",
  "health_check_path": "/api/health",
  "health_check_key": "status",
  "health_check_value": "ok",
  "repo_url": "https://github.com/owner/repo",
  "app_version": "0.4.0",
  "update_check_enabled": true,
  "internal_ports": { "nginx": 80 },
  "env_internal_port_keys": { "nginx": "NGINX_PORT" },
  "show_advanced_ports": true,
  "locale": "en"
}
```

> `internal_ports`, `env_internal_port_keys`, and `show_advanced_ports` are
> optional expert fields — omit them and the launcher behaves exactly as before
> (single host port, no advanced panel).

## Features

- **One persistent window** — never closes itself; only the X closes it.
- **Docker check on startup** — distinguishes *not installed* / *running* / *stopped* / *no Docker*.
- **Live build progress** — the Docker build is streamed line-by-line into the window.
- **Configurable port** — editable in the GUI and via `--port`, validated and persisted (to `launcher.json` and the `.env` next to the compose file, so the launcher and Compose can't disagree).
- **Live port changes** — the port field stays editable while the app runs; "Apply port" validates, rewrites `.env`, and recreates the stack in seconds (no rebuild — only the published host port changed), then health-checks on the **new** port.
- **Advanced internal ports** (experts) — optional `internal_ports` / `env_internal_port_keys` let you remap in-container ports (full 1–65535 range, e.g. nginx `:80`); a collapsed "Advanced settings" panel (gated by `show_advanced_ports`) applies them with an image rebuild + health check. Empty by default: no extra `.env` keys, no UI, no behaviour change.
- **Verified actions** — install runs a health check; uninstall re-lists the containers to confirm they are gone.
- **Install manifest + startup cleanup** — finds and offers to remove stale leftovers of older installs.
- **Verbose uninstall / cleanup** — every step reported with a ✓ / ✗ result.
- **Single-instance guard** — a PID-based lockfile refuses a second launch with an "already running" notice instead of opening a duplicate window.
- **Background update check** — checks GitHub Releases (derived from `repo_url`) and notes in-window when a newer release exists. Opt-out via `update_check_enabled`; silent on any network error.
- **File logging** — a rotated `launcher.log` plus a per-run `install.log` under the config dir, and a `launcher-debug.log` on `--debug`. Best-effort: an unwritable dir degrades gracefully rather than crashing.
- **Concurrency-safe UI** — every button is disabled for the full duration of an action and the window is held on top, so a second action can't be launched in parallel.
- **Quiet on Windows** — every Docker subprocess runs with `CREATE_NO_WINDOW`, so an install no longer flashes a swarm of console windows (no change on Linux/macOS).
- **PyInstaller-ready** — a bundled spec template, hidden-imports list, and build-time version injection for shipping frozen single-file builds.
- **System tray + "Run in background"** (optional) — `pip install docker-app-launcher[tray]`; while running, the window minimizes to the tray (a visible **Run in the background** button and the X both use it). When the tray can't dock it falls back to a taskbar-minimized window, so it never silently closes.
  - **Linux note:** the reliable tray uses pystray's **AppIndicator** backend, which needs `gi` (PyGObject) plus the AppIndicator typelib. The `[tray]` extra pip-installs PyGObject (Linux-only; needs `libgirepository1.0-dev`, `libcairo2-dev`, `pkg-config` to build), and you also need the typelib at runtime — on Ubuntu: `sudo apt install gir1.2-ayatanaappindicator3-0.1`. If you instead rely on the system `python3-gi` (apt), create the venv with `--system-site-packages` so `gi` is importable. Without any of this the launcher still works — it just minimizes to the taskbar. Run with `--debug` to see which backend was selected.
- **DE / EN i18n (YAML)** — strings live in per-language YAML files (`i18n/de.yaml`, `i18n/en.yaml`) loaded at startup; **add a language by dropping a `<code>.yaml` file** beside them. German uses real UTF-8 umlauts. Per-app overrides via `custom_strings`.
- **Actions architecture** — all business logic is GUI-free and unit-tested without a display.
- **CLI ↔ GUI parity** — both call the exact same actions layer.

## Custom Icons

Configure window and system tray icons:

```python
launch(LauncherConfig(
    app_name="My App",
    icon_path="path/to/app-icon.png",         # Window icon
    tray_icon_path="path/to/tray-icon.png",   # Tray icon (optional, falls back to icon_path)
))
```

```json
{
  "icon_path": "branding/my-app-icon.png",
  "tray_icon_path": "branding/my-app-tray.png"
}
```

If no icon is configured, a default icon with the app's initial letter is generated automatically.

Supported formats: PNG (recommended), ICO, BMP. Recommended size: 256x256 (window), 64x64 (tray).

## Cleanup Configuration

Configure which paths are searched for stale artifacts:

```python
launch(LauncherConfig(
    app_name="My App",
    container_name="my-app",
    legacy_names=["old-app-name", "prototype-v1"],
    cleanup_configs=[
        "~/.old-app-name",
        "~/.config/old-app-name",
        "~/.local/share/old-app-name",
    ],
    cleanup_search_paths=[
        "~/.config/",
        "~/.local/share/",
        "~/",
    ],
))
```

```json
{
  "legacy_names": ["old-app-name"],
  "cleanup_configs": [
    "~/.old-app-name",
    "~/.config/old-app-name"
  ],
  "cleanup_search_paths": [
    "~/.config/",
    "~/.local/share/",
    "~/"
  ]
}
```

- `legacy_names`: Previous project names to find stale containers/images/volumes.
- `cleanup_configs`: Explicit config directories to offer for removal.
- `cleanup_search_paths`: Base directories searched for `legacy_names` subdirectories (`<base>/<name>` and `<base>/.<name>`).
- Active project volumes are automatically excluded from cleanup.
- User-data volumes are unchecked by default (opt-in deletion).

## Configuration Paths

All launcher state is stored under `config_dir` (default: `~/.{app_slug}/`):

```
~/.my-app/
  launcher.json          # Port, settings, preferences
  .env                   # Docker Compose port variables
  install-manifest.json  # Installed containers, images, history
  launcher.log           # Persistent log (rotated, 5MB max)
  install.log            # Last install/rebuild log
  launcher.lock          # Single-instance lockfile
```

Override the config directory:

```python
launch(LauncherConfig(
    config_dir="~/.custom-path/my-app",
))
```

## Install Manifest

The launcher automatically maintains an install manifest at `{config_dir}/install-manifest.json`. This file tracks every artifact created during installation, enabling precise cleanup without guesswork.

```json
{
  "installed_at": "2026-06-24T14:30:00Z",
  "updated_at": "2026-06-24T18:15:00Z",
  "app_name": "My App",
  "app_version": "1.95.0",
  "launcher_version": "0.5.0",
  "port": 8501,
  "compose_project": "my-app",
  "compose_file": "/home/user/my-app/docker-compose.prod.yml",
  "containers": [
    {"name": "my-app-frontend", "image": "my-app-frontend:latest"},
    {"name": "my-app-backend", "image": "my-app-backend:latest"}
  ],
  "images": [
    "my-app-frontend:latest",
    "my-app-backend:latest"
  ],
  "volumes": [
    "my-app-data"
  ],
  "install_history": [
    {"action": "install", "version": "1.94.0", "at": "2026-06-20T10:00:00Z"},
    {"action": "update", "version": "1.95.0", "at": "2026-06-24T14:30:00Z"}
  ]
}
```

The manifest is:
- **Written** after every successful install or start (with rebuild).
- **Updated** with version and timestamp on each start.
- **Appended** to `install_history` for every install/update/uninstall.
- **Marked** as uninstalled (not deleted) on deinstallation.

### How cleanup uses the manifest

With a manifest, cleanup knows exactly which containers, images and volumes belong to the current or previous installation. Without a manifest (legacy installs), it falls back to pattern-matching against `container_name` and `legacy_names`.

```
Cleanup with manifest:    Precise — removes listed artifacts only
Cleanup without manifest: Pattern-based — searches by name patterns
```

This is why the manifest is created automatically and should not be deleted manually.

## Architecture

| Module        | Responsibility                                              |
|---------------|-------------------------------------------------------------|
| `config.py`         | `LauncherConfig` dataclass — the single source of truth.    |
| `actions.py`        | All business logic. No `tkinter`. Fully testable.           |
| `gui.py`            | `LauncherApp(tk.Tk)` — a thin window over the actions.       |
| `tray.py`           | Optional system tray (pystray + Pillow).                     |
| `i18n.py`           | DE/EN strings with custom-string overrides.                  |
| `lockfile.py`       | PID-based single-instance guard.                            |
| `update_check.py`   | Background GitHub Releases update check.                    |
| `logging_setup.py`  | Rotated file logging (`launcher.log` / `install.log`).      |
| `subprocess_utils.py` | Windows `CREATE_NO_WINDOW` kwargs for all subprocesses.   |
| `pyinstaller/`      | Spec template + helpers for frozen builds.                  |
| `__main__.py`       | CLI entry point + GUI router.                                |

## Configuration reference

See [LauncherConfig](https://github.com/astrapi69/docker-app-launcher/blob/main/src/docker_app_launcher/config.py) for the full field list
(app identity, network/health, Docker timeouts, paths, GUI, links, cleanup,
tray, i18n, and lifecycle callbacks).

## Development

```bash
poetry install --with dev --all-extras
make ci        # lint + format-check + typecheck + tests
make test      # tests with coverage
make fix       # auto-fix lint + format
```

## Used by

- [Adaptive Learner](https://github.com/astrapi69/adaptive-learner)

## License

[MIT](https://github.com/astrapi69/docker-app-launcher/blob/main/LICENSE) © Asterios Raptis

