# Docker Deployment Guide

> **Proxmox VE Users:** Consider using the [official installer](https://github.com/rcourtman/Pulse#install) instead, which automatically creates an optimized LXC container.

## Quick Start

```bash
docker run -d \
  --name pulse \
  -p 7655:7655 \
  -v pulse_data:/data \
  --restart unless-stopped \
  rcourtman/pulse:latest
```

1. Inspect the bootstrap token generated inside the container: `docker exec -it pulse cat /data/.bootstrap_token`
2. Access `http://your-server:7655`
3. Paste the bootstrap token when prompted, then **complete the mandatory security setup**
4. Save the generated credentials and API token—they are shown only once and the bootstrap file is deleted after success

## First-Time Setup

When you first access Pulse, you'll see the security setup wizard:

1. **Create Admin Account**
   - Choose a username (default: admin)
   - Set a password or use the generated one
   - An API token is automatically generated

2. **Save Your Credentials**
   - Download or copy them immediately
   - They won't be shown again after setup

3. **Access Dashboard**
   - Click "Continue to Login"
   - Use your new credentials to sign in

## Docker Compose

### Basic Setup (Recommended for First-Time Users)

```yaml
services:
  pulse:
    image: rcourtman/pulse:latest
    container_name: pulse
    ports:
      - "7655:7655"
    volumes:
      - pulse_data:/data
    restart: unless-stopped

volumes:
  pulse_data:
```

Then:
1. Run: `docker compose up -d`
2. Access: `http://your-server:7655`
3. Complete the security setup wizard
4. (Optional) Copy `.env.example` to `.env` if you want to pre-configure credentials later

### Pre-Configured Authentication (Advanced)

If you want to skip the setup wizard, you can pre-configure authentication:

```yaml
services:
  pulse:
    image: rcourtman/pulse:latest
    container_name: pulse
    ports:
      - "7655:7655"
    volumes:
      - pulse_data:/data
    environment:
      PULSE_AUTH_USER: 'admin'
      # Plain text values are auto-hashed on startup. To use a bcrypt hash,
      # escape $ as $$ (e.g. $$2a$$12$$...) so docker compose does not treat it
      # as variable expansion.
      PULSE_AUTH_PASS: 'super-secret-password'
      # Provide one or more API tokens. Tokens can be raw values or SHA3-256 hashes.
      # Use distinct tokens per automation target for easier revocation.
      API_TOKENS: 'ansible-token,docker-agent-token'
      # Optional legacy variable kept for compatibility; newest token is used if both are set.
      # API_TOKEN: 'your-48-char-hex-token'
      PULSE_PUBLIC_URL: 'https://pulse.example.com'  # Used for webhooks/links
      # Optional logging controls (v4.24.0+)
      LOG_LEVEL: 'info'
      LOG_FORMAT: 'auto'               # auto | json | console
      # LOG_FILE: '/data/pulse.log'    # uncomment to mirror logs to a file
      # LOG_MAX_SIZE: '100'            # MB
      # LOG_MAX_AGE: '30'              # days
      # LOG_COMPRESS: 'true'
      # TZ: 'UTC'
    restart: unless-stopped

volumes:
  pulse_data:
```

⚠️ **Important**: If you paste a bcrypt hash instead of a plain-text password, remember that Compose treats `$` as variable expansion. Escape each `$` as `$$`. Example: `$2a$12$...` becomes `$$2a$$12$$...`.

### Using External .env File (Cleaner Approach)

Create `.env` file (no escaping needed here). You can copy `.env.example` from the repository as a starting point:
```env
PULSE_AUTH_USER=admin
PULSE_AUTH_PASS=super-secret-password          # Plain text (auto-hashed) or bcrypt hash
# Optional legacy token (used if API_TOKENS is empty)
API_TOKEN=your-48-char-hex-token               # Generate with: openssl rand -hex 24
# Comma-separated list of tokens for automation/agents
API_TOKENS=${ANSIBLE_TOKEN},${DOCKER_AGENT_TOKEN}
PULSE_PUBLIC_URL=https://pulse.example.com     # Recommended for webhooks
TZ=Asia/Kolkata                                # Optional: matches host timezone
# Logging controls (optional; take effect immediately after restart)
LOG_LEVEL=info
LOG_FORMAT=auto
# LOG_FILE=/data/pulse.log
# LOG_MAX_SIZE=100
# LOG_MAX_AGE=30
# LOG_COMPRESS=true
```

**Note**: Plain text credentials are automatically hashed for security. You can provide either plain text (simpler) or pre-hashed values (advanced).

Docker-compose.yml:
```yaml
services:
  pulse:
    image: rcourtman/pulse:latest
    container_name: pulse
    ports:
      - "7655:7655"
    volumes:
      - pulse_data:/data
    env_file: .env
    restart: unless-stopped

volumes:
  pulse_data:
```

### Updating Your Stack

```bash
docker compose pull        # Fetch the latest Pulse image
docker compose up -d       # Recreate container with zero-downtime update
```

If you change anything in `.env`, run `docker compose up -d` again so the container picks up the new values.

## Generating Credentials (Optional)

**Note**: Since v4.5.0, plain text credentials are automatically hashed. Pre-hashing is optional for advanced users.

### Simple Approach (Recommended)
```bash
# Just use plain text - Pulse auto-hashes for you
docker run -d \
  -e PULSE_AUTH_USER=admin \
  -e PULSE_AUTH_PASS=mypassword \
  -e API_TOKENS="ansible-token,docker-agent-token" \
  rcourtman/pulse:latest
```

> Tip: Create one token per automation workflow (Ansible, Docker agents, CI jobs, etc.) so you can revoke individual credentials without touching others. Use **Settings → Security → API tokens** or `POST /api/security/tokens` to mint tokens programmatically.

### Advanced: Pre-Hashing (Optional)
```bash
# Generate bcrypt hash for password
docker run --rm -it rcourtman/pulse:latest pulse hash-password

# Generate random API tokens
ANSIBLE_TOKEN=$(openssl rand -hex 32)
DOCKER_AGENT_TOKEN=$(openssl rand -hex 32)
# Then pass them to the container via API_TOKENS
```

## Data Persistence

All configuration and data is stored in `/data`:
- `.env` - Authentication credentials (if using setup wizard)
- `*.enc` - Encrypted node credentials
- `*.json` - Configuration files
- `.encryption.key` - Auto-generated encryption key

### Backup
```bash
docker run --rm -v pulse_data:/data -v $(pwd):/backup alpine tar czf /backup/pulse-backup.tar.gz -C /data .
```

### Restore
```bash
docker run --rm -v pulse_data:/data -v $(pwd):/backup alpine tar xzf /backup/pulse-backup.tar.gz -C /data
```

## Docker Workspace Highlights

Once the agent is reporting, open the **Docker** tab in Pulse to explore:

- **Host grid with issues column** – surfaces restart loops, health-check failures, and highlights hosts that have missed their heartbeat.
- **Inline search** – filter by host name, stack label, or container name; results update instantly in the grid and side drawer.
- **Container drawers** – show CPU/memory charts, restart counters, last exit codes, mounted ports, and environment labels at a glance.
- **Time-since heartbeat** – every host entry shows the last heartbeat timestamp so you can spot telemetry gaps quickly.

If a host remains offline, review [Troubleshooting → Docker Agent Shows Hosts Offline](TROUBLESHOOTING.md#docker-agent-shows-hosts-offline).

## Network Discovery

Pulse automatically scans common home/office networks when running in Docker.

### How It Works
1. Detects Docker environment automatically
2. Scans multiple common subnets in parallel:
   - 192.168.1.0/24 (most routers)
   - 192.168.0.0/24 (very common)
   - 10.0.0.0/24 (some setups)
   - 192.168.88.0/24 (MikroTik)
   - 172.16.0.0/24 (enterprise)

**Result**: Finds all Proxmox nodes without any configuration!

### Custom Networks (Rarely Needed)
Only for non-standard subnets:
```yaml
environment:
  DISCOVERY_SUBNET: "192.168.50.0/24"  # Only if using unusual subnet
```

## Common Issues

### Can't Access After Upgrade
If upgrading from pre-v4.5.0:
1. You'll see the security setup wizard
2. Complete the setup - your nodes are preserved
3. Use your new credentials to login

### Lost Credentials
If you've lost your credentials:
```bash
# Stop container
docker stop pulse

# Remove auth configuration
docker exec pulse rm /data/.env

# Restart and go through setup again
docker restart pulse
```

### Setup Wizard Not Showing
This happens if you have auth environment variables set:
1. Remove environment variables from docker-compose.yml
2. Recreate the container
3. Access the UI to see the setup wizard

### Password Hash Issues
Common problems:
- **Hash truncated**: Must be exactly 60 characters
- **Not escaped in docker-compose**: Use `$$` instead of `$`
- **Wrong format**: Must start with `$2a$`, `$2b$`, or `$2y$`

## Security Best Practices

1. **Always use HTTPS in production** - Use a reverse proxy (nginx, Traefik, Caddy)
2. **Strong passwords** - Use the generated password or 16+ characters
3. **Protect API tokens** - Treat them like passwords
4. **Regular backups** - Backup the `/data` volume regularly
5. **Network isolation** - Don't expose port 7655 directly to the internet

## Updates & Rollbacks (v4.24.0+)

Docker images are still updated manually, but Pulse now records every upgrade/rollback attempt in **Settings → System → Updates** alongside the CLI instructions below.

### Update to the latest image
```bash
docker pull rcourtman/pulse:latest
docker stop pulse
docker rm pulse
docker run -d --name pulse \
  -p 7655:7655 \
  -v pulse_data:/data \
  --restart unless-stopped \
  rcourtman/pulse:latest
```
- The update history entry includes the image tag, operator, and timestamps. Capture the `event_id` in your change log.
- After the container is back online, verify the adaptive scheduler is healthy:
  ```bash
  curl -s http://localhost:7655/api/monitoring/scheduler/health \
    | jq '.queue.depth'
  ```

### Roll back to a prior release
- Choose a previous tag (for example `v4.23.2`) from [GitHub Releases](https://github.com/rcourtman/Pulse/releases) or Docker Hub.
- Redeploy the container with that tag:
  ```bash
  docker pull rcourtman/pulse:v4.23.2
  docker stop pulse && docker rm pulse
  docker run -d --name pulse \
    -p 7655:7655 \
    -v pulse_data:/data \
    --restart unless-stopped \
    rcourtman/pulse:v4.23.2
  ```
- The update history will log this as a rollback. Make sure to annotate the entry with the reason in your postmortem notes.

> **Tip:** Keep the last known-good tag handy (for example in your compose file or infra repo) so rollbacks are a single change.

## Environment Variables Reference

> **⚠️ Important**: Environment variables always override UI/system.json settings. If you set a value via env var (e.g., `DISCOVERY_SUBNET`), changes made in the UI for that setting will NOT take effect until you remove the env var. This follows standard container practices where env vars have highest precedence.

### Authentication
| Variable | Description | Example / Default |
|----------|-------------|-------------------|
| `PULSE_AUTH_USER` | Admin username | `admin` |
| `PULSE_AUTH_PASS` | Admin password (plain text auto-hashed or bcrypt hash) | `super-secret-password` or `$2a$12$...` |
| `API_TOKEN` | Legacy single API token (optional fallback) | `openssl rand -hex 24` |
| `API_TOKENS` | Comma-separated list of API tokens (plain or SHA3-256 hashed) | `ansible-token,docker-agent-token` |
| `PULSE_AUDIT_LOG` | Enable security audit logging | `false` |

> Locked out while testing a container? Create `/data/.auth_recovery`, restart the container, and connect from localhost to reset credentials. Remove the flag file and restart again to restore normal authentication.

### Network
| Variable | Description | Default |
|----------|-------------|---------|
| `FRONTEND_PORT` | Port exposed for the UI inside the container | `7655` |
| `BACKEND_PORT` | API port (same as UI for the all-in-one container) | `7655` |
| `BACKEND_HOST` | Bind address for the backend | `0.0.0.0` |
| `PULSE_PUBLIC_URL` | External URL used in notifications/webhooks | *(unset)* |
| `ALLOWED_ORIGINS` | Additional CORS origins (comma separated) | Same-origin only |
| `DISCOVERY_SUBNET` | Override automatic network discovery CIDR | Auto-scans common networks |
| `CONNECTION_TIMEOUT` | Proxmox/PBS API timeout (seconds) | `10` |
| `PORT` | Legacy alias for `FRONTEND_PORT` | `7655` |

### System
| Variable | Description | Default |
|----------|-------------|---------|
| `TZ` | Timezone inside the container | `UTC` |
| `LOG_LEVEL` | Logging verbosity. Changing the env var and restarting updates Pulse immediately. | `info` |
| `LOG_FORMAT` | `auto`, `json`, or `console` output format. | `auto` |
| `LOG_FILE` | Optional path inside the container to mirror logs (e.g. `/data/pulse.log`). Empty = stdout only. | *(unset)* |
| `LOG_MAX_SIZE` | Rotate `LOG_FILE` after this size (MB). | `100` |
| `LOG_MAX_AGE` | Days to retain rotated log files. | `30` |
| `LOG_COMPRESS` | Compress rotated log files (`true` / `false`). | `true` |
| `METRICS_RETENTION_DAYS` | Days of metrics history to keep | `7` |

## Advanced Configuration

### Custom Network
```yaml
services:
  pulse:
    image: rcourtman/pulse:latest
    networks:
      - monitoring
    # ... rest of config

networks:
  monitoring:
    driver: bridge
```

### Resource Limits
```yaml
services:
  pulse:
    image: rcourtman/pulse:latest
    deploy:
      resources:
        limits:
          cpus: '0.5'
          memory: 256M
    # ... rest of config
```

### Health Check
```yaml
services:
  pulse:
    image: rcourtman/pulse:latest
    healthcheck:
      test: ["CMD", "wget", "--spider", "-q", "http://localhost:7655/api/health"]
      interval: 30s
      timeout: 10s
      retries: 3
    # ... rest of config
```
