Metadata-Version: 2.4
Name: redock-vps
Version: 0.1.2
Summary: One-command Docker deployment to Hetzner VPS
License: MIT License
        
        Copyright (c) 2026 Florent Pigout
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
License-File: LICENSE
Requires-Python: >=3.11
Requires-Dist: click>=8.1
Requires-Dist: dnspython>=2.6
Requires-Dist: docker>=7.1
Requires-Dist: fabric>=3.2
Requires-Dist: hcloud>=2.0
Requires-Dist: pydantic-settings>=2.4
Requires-Dist: requests>=2.31
Requires-Dist: rich>=13.7
Requires-Dist: typer>=0.12
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: ruff>=0.6; extra == 'dev'
Description-Content-Type: text/markdown

# redock

One command from zero to a live HTTPS app on a Hetzner VPS.

```bash
redock up excalidraw --slug drawing
```

That's it. redock provisions a server, installs Docker and a Caddy reverse proxy, handles DNS, deploys the container, and hands you back a live URL.

## How it works

1. **Provision** — finds or creates a Hetzner `cx22` (or your chosen type)
2. **Prepare** — SSHes in, installs Docker + `caddy-docker-proxy` via Docker Compose
3. **DNS** — creates the `A` record automatically (Hetzner DNS) or prints it for manual entry, then polls until it resolves
4. **Deploy** — runs the container with Caddy labels; Caddy gets a Let's Encrypt cert automatically
5. **Verify** — polls HTTPS until the app responds `200`, then prints the URL

The VPS is shared across deployments — each app is one container behind Caddy.

## Prerequisites

- A [Hetzner Cloud](https://www.hetzner.com/cloud) account and API token
- An SSH key uploaded to Hetzner (default name: `redock-key`)
- A domain you can add DNS records to
- Python 3.11+ and [uv](https://docs.astral.sh/uv/)

For automatic DNS (optional): a [Hetzner DNS](https://www.hetzner.com/dns-console) account with a zone for your domain.

## Install

```bash
uv pip install redock
```

Or from source:

```bash
git clone https://gitlab.com/toopy/redock
cd redock
uv sync
```

## Configuration

Copy `.env.example` to `.env` and fill in the required values:

```bash
cp .env.example .env
```

| Variable | Required | Default | Description |
|----------|----------|---------|-------------|
| `REDOCK_HETZNER_TOKEN` | yes | — | Hetzner Cloud API token |
| `REDOCK_BASE_DOMAIN` | yes | — | Domain for deployments, e.g. `apps.example.com` |
| `REDOCK_ACME_EMAIL` | yes | — | Email for Let's Encrypt notifications |
| `REDOCK_HETZNER_SSH_KEY_NAME` | no | `redock-key` | SSH key name in Hetzner Cloud |
| `REDOCK_SSH_KEY_PATH` | no | `~/.ssh/id_ed25519` | Path to local private key |
| `REDOCK_HETZNER_REGION` | no | `fsn1` | Hetzner datacenter |
| `REDOCK_HETZNER_SERVER_TYPE` | no | `cx22` | Server type |
| `REDOCK_STATE_FILE` | no | `~/.redock/state.json` | Where deployment state is stored |
| `REDOCK_CATALOG_DB` | no | `~/.redock/catalog.db` | Catalog SQLite database path |
| `REDOCK_INFOMANIAK_API_KEY` | no | — | Required for `catalog update --use-ai` |
| `REDOCK_INFOMANIAK_PRODUCT_ID` | no | — | Required for `catalog update --use-ai` |
| `REDOCK_INFOMANIAK_CHAT_MODEL` | no | `google/gemma-4-31B-it` | LLM model for metadata extraction |
| `REDOCK_DOCKERHUB_USERNAME` | no | — | Docker Hub username for private image metadata |
| `REDOCK_DOCKERHUB_TOKEN` | no | — | Docker Hub access token (read-only) for private image metadata |
| `REDOCK_SCAN_TRIVY_WEIGHT` | no | `0.5` | Trivy's share of the combined scan score |
| `REDOCK_SCAN_SCOUT_WEIGHT` | no | `0.5` | Docker Scout's share of the combined scan score |
| `REDOCK_SCAN_SEV_CRITICAL` | no | `5` | Penalty coefficient per CRITICAL vulnerability |
| `REDOCK_SCAN_SEV_HIGH` | no | `2` | Penalty coefficient per HIGH vulnerability |
| `REDOCK_SCAN_SEV_MEDIUM` | no | `0.5` | Penalty coefficient per MEDIUM vulnerability |
| `REDOCK_SCAN_SEV_LOW` | no | `0.1` | Penalty coefficient per LOW vulnerability |

Then verify everything looks good:

```bash
redock doctor
```

## Automatic DNS with Hetzner DNS

redock uses `REDOCK_HETZNER_TOKEN` for both VPS provisioning and DNS management. If your domain zone exists in [console.hetzner.com](https://console.hetzner.com) → **Networking → DNS**, redock will create or update the `A` record automatically on every `redock up`.

**Setup:** Add your domain zone in the Hetzner Console and point your registrar's nameservers to Hetzner's — no extra token needed.

> **Note:** redock uses the [Hetzner Cloud DNS API](https://docs.hetzner.cloud/reference/cloud#tag/zones) (`api.hetzner.cloud/v1`). The zone must already exist — redock manages records within a zone, not the zone itself. It discovers the right zone by walking the hostname labels (e.g. for `drawing.apps.example.com` it tries `apps.example.com`, then `example.com`).

## A real example

Deploy [Excalidraw](https://excalidraw.com) as `drawing.apps.example.com`:

```bash
redock up excalidraw --slug drawing
```

```text
● Ensuring VPS
● Preparing host (Docker + Caddy)
● DNS records — creating and waiting for propagation
✓ Created DNS A record: drawing.apps.example.com → 65.21.104.33
● Deploying container
● Waiting for HTTPS
✓ Live: https://drawing.apps.example.com

Done! https://drawing.apps.example.com
```

The `--slug` flag sets the subdomain prefix. If omitted it defaults to the template name:

```bash
redock up <id>                          # → <id>.<base_domain>
redock up <id> -s <slug>               # → <slug>.<base_domain>
redock up <id> --env KEY=VALUE         # override an env var at deploy time
redock up <id> --upgrade               # stop existing container, pull latest image, redeploy
```

## Multi-container apps — docker swarm stacks

For apps that ship as more than one container (a service + a worker, an
API + a frontend, anything you'd model as a `docker-compose.yml`),
redock can deploy the whole stack via docker swarm. The same VPS hosts
multiple stacks side-by-side, all behind the shared Caddy.

```bash
redock stack up ./stack.yml --slug myapp
# → https://myapp.<base_domain>
```

What it does:

1. **Ensure VPS** + **prepare host** — same as `redock up`, but `prepare`
   also runs `docker swarm init` (idempotent) and ensures `redock_net`
   is an `overlay --attachable` network so swarm services AND the Caddy
   container can both attach to it.
2. **DNS** — same automatic `A` record creation as single-container deploys.
3. **Deploy stack** — uploads your YAML to `/opt/redock/stacks/<slug>.yml`
   and runs `docker stack deploy -c <file> <slug>` over SSH. Re-running
   updates the services in place (docker's native rolling update).
4. **Wait HTTPS** — polls until the public URL responds.

### Stack file requirements

Your `stack.yml` is a normal docker compose v3+ file with two
additions:

- The publicly-reachable service must declare the **Caddy labels** so
  caddy-docker-proxy routes the hostname to it:

  ```yaml
  services:
    web:
      image: yourorg/web:latest
      networks:
        - redock_net
      deploy:
        labels:
          - "caddy=$DOMAIN"
          - "caddy.reverse_proxy={{upstreams 8080}}"
  ```

- All services that must be on the Caddy network reference the external
  attachable overlay:

  ```yaml
  networks:
    redock_net:
      external: true
  ```

`$DOMAIN` (and any other `${VAR}`) get interpolated from `--env KEY=VAL`
at deploy time. redock substitutes them via the shell so compose's
native interpolation picks them up. See `swarm-me`'s `stack.yml` for a
real 4-service example.

### Stack commands

```bash
redock stack up ./stack.yml --slug myapp           # deploy / update
redock stack up ./stack.yml --slug myapp \
  --env DOMAIN=myapp.deploy-me.eu                  # env substitution
redock stack up ./stack.yml --slug myapp \
  --registry-auth 'registry.gitlab.com::ci-token::glpat-xxx'  # private registry pull
redock stack rm myapp                              # take it down (keeps VPS)
redock stack list                                  # what's running on the shared VPS
```

Stacks coexist with single-container `redock up` deployments on the same
VPS — Caddy reads labels from both swarm service tasks and plain
containers via the shared `redock_net` overlay.

## App catalog

```bash
# Add an app (all flags optional; Sub-project 2 will fill them via AI)
redock catalog add nginx:latest --port 80
redock catalog add louislam/uptime-kuma:latest --port 3001 --volume /app/data

# Store environment variables on the catalog item (repeatable)
redock catalog add myapp:latest --port 8080 --env DB_URL=postgres://localhost/db --env DEBUG=false
redock catalog update myapp --env SECRET_KEY=abc123   # merges with existing env vars

# Reference a host env var — prefix value with $ (use single quotes to prevent shell expansion)
redock catalog update myapp --env 'DB_PASS=$MY_SECRET'   # stored as-is; resolved at deploy time

# Mark as ready to deploy (or uncheck with --no)
redock catalog check nginx
redock catalog check --no nginx

# List, inspect, update, remove
redock catalog list
redock catalog list --checked-only
redock catalog show nginx
redock catalog update nginx --port 8080
redock catalog delete nginx

# Search the local catalog
redock catalog search ngi

# Scan an image for vulnerabilities (Trivy + Docker Scout)
redock catalog scan nginx
redock catalog scan nginx --dry-run   # scan without saving results
redock catalog scan nginx --verbose   # show per-scanner breakdown
```

## Security scanning

`redock catalog scan` runs [Trivy](https://trivy.dev) and [Docker Scout](https://docs.docker.com/scout/) against the image, merges their findings, and computes a weighted 0–100 safety score. At least one scanner must be installed — the command skips unavailable scanners gracefully and only fails if none can run.

### Install Trivy

```bash
task trivy:install
```

This runs the official install script and places `trivy` in `~/.local/bin` (no sudo needed). Alternatively, install manually:

- macOS: `brew install aquasecurity/trivy/trivy`
- Linux: `curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b ~/.local/bin`

Verify: `trivy --version`

### Install Docker Scout

```bash
task scout:install
```

This installs the Docker Scout CLI plugin to `~/.docker/cli-plugins`. Docker Desktop 4.17+ already bundles it — run `docker scout version` to check before installing.

Verify: `docker scout version`

> **Note:** `redock up` requires the template to exist in the catalog **and** be marked as checked.
> A missing port also blocks deployment.

## Other commands

```bash
# See all running deployments
redock list

# Stop a deployment (keeps container and state, can be restarted)
redock stop <slug>

# Remove container + volume + state + DNS record, destroy VPS if no apps remain
redock purge <slug>
redock purge <slug> --yes  # skip confirmation

# VPS - Step-by-step control
redock vps create         # provision VPS only
redock vps install        # install Docker + Caddy only
redock vps show           # print VPS IP and status
redock vps delete         # destroy VPS (blocked if deployments still exist)

# DNS - Step-by-step control
redock dns show <slug>    # print required DNS record
redock dns update <slug>  # create/update A record (Hetzner DNS)
redock dns wait <slug>    # wait until DNS resolves
redock dns clean <slug>   # delete A record (Hetzner DNS)
```

## License

MIT
