# Dokploy Deployment Guide — BukidBountyApp (Hypervel/Swoole)

> This guide documents the exact process for deploying BukidBountyApp (and derivative projects) on a Dokploy-managed server. It is based on real troubleshooting and verified configuration.

---

## Table of Contents

1. [Architecture Overview](#architecture-overview)
2. [Prerequisites on Dokploy Server](#prerequisites-on-dokploy-server)
3. [Step 1: Create Infrastructure Services](#step-1-create-infrastructure-services)
4. [Step 2: Deploy the Application](#step-2-deploy-the-application)
5. [Step 3: Configure Environment Variables](#step-3-configure-environment-variables)
6. [Step 4: Configure Domain & HTTPS](#step-4-configure-domain--https)
7. [Step 5: Create the Database](#step-5-create-the-database)
8. [Step 6: Run Migrations & Seed](#step-6-run-migrations--seed)
9. [Deploying Test/Beta Variants](#deploying-testbeta-variants)
10. [Troubleshooting](#troubleshooting)
11. [Reference: Working Configuration Files](#reference-working-configuration-files)

---

## Architecture Overview

```
Internet → Traefik (ports 80/443) → dokploy-network → bukidapp container (port 9501)
                                                     → DragonflyDB (port 6379)
                                                     → MySQL (port 3306)
```

| Component         | Type                     | Network          |
|-------------------|--------------------------|------------------|
| BukidBountyApp    | Docker Compose (Dokploy) | `dokploy-network`|
| MySQL 8           | Dokploy Database (Swarm) | `dokploy-network`|
| DragonflyDB       | Docker Compose (Dokploy) | `dokploy-network`|
| Traefik           | Managed by Dokploy       | `dokploy-network`|

---

## Prerequisites on Dokploy Server

- Dokploy installed and running
- Traefik configured with Let's Encrypt cert resolver
- DNS pointing your domain to the server IP

---

## Step 1: Create Infrastructure Services

### 1a. Create MySQL Database

1. In Dokploy UI → **Create** → **Database** → **MySQL**
2. Set root password, default user, and password
3. Dokploy will create a Swarm service (e.g., `prodservers-prodsqlmain-kv3yph`)

> **⚠️ IMPORTANT:**
> The `DB_HOST` for your app is the **Swarm service name**, not the container name.
> For Swarm services, use the service name directly (e.g., `prodservers-prodsqlmain-kv3yph`).

**Find the MySQL service name (run on Hostinger server terminal):**
```bash
docker service ls | grep -i sql
```

### 1b. Create DragonflyDB (Redis Alternative)

1. In Dokploy UI → **Create** → **Compose** → new project for DragonflyDB
2. Use this `docker-compose.yml`:

```yaml
version: '3.8'
services:
  dragonflydb:
    image: 'docker.dragonflydb.io/dragonflydb/dragonfly'
    ulimits:
      memlock: -1
    ports:
      - "6379:6379"
    volumes:
      - dragonflydata:/data
    environment: 
      - DFLY_requirepass
    networks:
      - dokploy-network

volumes:
  dragonflydata:

networks:
  dokploy-network:
    external: true
```

3. Deploy the service

> **⚠️ IMPORTANT:**
> The `REDIS_HOST` for your app is the **full container name**, not the Dokploy project name.
> For Docker Compose services, the container name follows the pattern: `{project-name}-{service-name}-{replica}`.

**Find the DragonflyDB container name (run on Hostinger server terminal):**
```bash
docker ps --format '{{.Names}}' | grep dragonfly
# Example output: prodservers-dragonflydb-rnfaje-dragonflydb-1
```

> **🔴 CRITICAL DISTINCTION:**
> - **Swarm services** (Dokploy Databases) → use the **service name** as hostname
> - **Compose services** (like DragonflyDB) → use the **container name** as hostname
> 
> These are different! Always verify with `docker service ls` or `docker ps --format '{{.Names}}'`.

---

## Step 2: Deploy the Application

1. In Dokploy UI → **Create** → **Compose**
2. Connect to your Git repository (e.g., `git@git.cr8.space:josh/BukidBountyApp.git`)
3. Set branch to `main`
4. The `docker-compose.yml` in the repo will be used automatically

### Working `docker-compose.yml`

```yaml
services:
  bukidapp:
    build:
      context: .
      dockerfile: Dockerfile.php
    ports:
      - 9501
    networks:
      - dokploy-network
    healthcheck:
      test: [ "CMD-SHELL", "curl -f http://localhost:9501/health || exit 1" ]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s

networks:
  dokploy-network:
    external: true
```

> **🔴 CAUTION:**
> Do **NOT** use host port mapping like `9522:9501`. Just expose the internal port `9501`.
> Traefik handles external routing via labels that Dokploy injects automatically.

---

## Step 3: Configure Environment Variables

In Dokploy UI → your service → **Environment** tab, set:

```env
# === Database (MySQL) ===
DB_CONNECTION=mysql
DB_HOST=<mysql-swarm-service-name>
DB_DATABASE=bukid
DB_PORT=3306
DB_USERNAME=mysql
DB_PASSWORD=<your-mysql-password>

# === Cache/Redis (DragonflyDB) ===
CACHE_DRIVER=redis
REDIS_HOST=<dragonflydb-container-name>
REDIS_AUTH=<your-dragonfly-password>
REDIS_PORT=6379
REDIS_DB=0
```

### How to Find the Correct Hostnames

| Service     | Type   | How to Find Hostname                                       | Example                                               |
|-------------|--------|------------------------------------------------------------|-------------------------------------------------------|
| MySQL       | Swarm  | `docker service ls \| grep sql`                            | `prodservers-prodsqlmain-kv3yph`                      |
| DragonflyDB | Compose| `docker ps --format '{{.Names}}' \| grep dragonfly`        | `prodservers-dragonflydb-rnfaje-dragonflydb-1`         |
| PostgreSQL  | Swarm  | `docker service ls \| grep pgsql`                          | `prodservers-prodpgsqlmain-x8frsw`                    |

> **💡 TIP:**
> You can verify connectivity from the app container using Dokploy's container terminal (UI):
> ```bash
> ping -c 2 <hostname>
> ```

---

## Step 4: Configure Domain & HTTPS

1. In Dokploy UI → your service → **Domains** tab
2. Add domain:
   - **Host**: `bukid.hesed.sbs` (or your domain)
   - **Container Port**: `9501`
   - **HTTPS**: Enabled
   - **Certificate**: Let's Encrypt
3. **Deploy/Redeploy** the service

### What Dokploy Adds Automatically

Dokploy injects these Traefik labels into your compose at deploy time:

```yaml
labels:
  - traefik.docker.network=dokploy-network
  - traefik.enable=true
  - traefik.http.routers.<id>-web.rule=Host(`bukid.hesed.sbs`)
  - traefik.http.routers.<id>-web.entrypoints=web
  - traefik.http.services.<id>-web.loadbalancer.server.port=9501
  - traefik.http.routers.<id>-web.middlewares=redirect-to-https@file
  - traefik.http.routers.<id>-websecure.rule=Host(`bukid.hesed.sbs`)
  - traefik.http.routers.<id>-websecure.entrypoints=websecure
  - traefik.http.services.<id>-websecure.loadbalancer.server.port=9501
  - traefik.http.routers.<id>-websecure.tls.certresolver=letsencrypt
```

> **⚠️ IMPORTANT:**
> You do **NOT** need to add these labels manually in your `docker-compose.yml`.
> Dokploy handles them when you configure the domain in the UI.
> Adding them manually can cause **label conflicts**.

---

## Step 5: Create the Database

The MySQL database must be created manually after the MySQL service is running.

**Via Hostinger server terminal:**

```bash
# Connect to MySQL container
docker exec -it $(docker ps --format '{{.Names}}' | grep prodsqlmain) mysql -u root -p
```

**Inside MySQL shell:**

```sql
CREATE DATABASE bukid;
GRANT ALL PRIVILEGES ON bukid.* TO 'mysql'@'%';
FLUSH PRIVILEGES;
EXIT;
```

---

## Step 6: Run Migrations & Seed

**Via Dokploy UI → your service → Terminal:**

```bash
php artisan migrate
php artisan db:seed
```

**Or from the Hostinger server terminal:**

```bash
docker exec <bukidapp-container-name> php artisan migrate
docker exec <bukidapp-container-name> php artisan db:seed
```

---

## Deploying Test/Beta Variants

To deploy a test or beta version of the same app with separate databases:

### 1. Plan Variant Resources

| Variant | MySQL DB Name | Redis DB | Domain                    |
|---------|---------------|----------|---------------------------|
| Prod    | `bukid`       | `0`      | `bukid.hesed.sbs`         |
| Beta    | `bukid_beta`  | `1`      | `beta.bukid.hesed.sbs`    |
| Test    | `bukid_test`  | `2`      | `test.bukid.hesed.sbs`    |

> **💡 TIP:**
> You can reuse the **same MySQL server** and **same DragonflyDB instance** for all variants.
> Just create different databases in MySQL and use different `REDIS_DB` numbers (0-15).

### 2. Create Additional MySQL Databases

**Via Hostinger server terminal:**

```bash
docker exec -it $(docker ps --format '{{.Names}}' | grep prodsqlmain) mysql -u root -p
```

```sql
CREATE DATABASE bukid_beta;
CREATE DATABASE bukid_test;
GRANT ALL PRIVILEGES ON bukid_beta.* TO 'mysql'@'%';
GRANT ALL PRIVILEGES ON bukid_test.* TO 'mysql'@'%';
FLUSH PRIVILEGES;
EXIT;
```

### 3. Deploy New Compose Project in Dokploy

1. In Dokploy UI → **Create** → **Compose**
2. Connect to the same Git repo, optionally use a different branch (e.g., `beta`, `develop`)
3. Set environment variables with variant-specific values:

```env
DB_DATABASE=bukid_beta
REDIS_DB=1
# DB_HOST and REDIS_HOST remain the same (same infrastructure)
```

4. Configure domain in Domains tab: `beta.bukid.hesed.sbs` → port `9501`
5. Deploy
6. Run migrations via Dokploy container terminal:
   ```bash
   php artisan migrate
   php artisan db:seed
   ```

### 4. Verify Each Variant

**Via Hostinger server terminal:**

```bash
# Find all bukid containers
docker ps --format '{{.Names}}' | grep bukid

# Check health of each — must show (healthy)
docker ps | grep bukid
```

---

## Troubleshooting

### 404 Not Found (Domain Access)

**Check this in order:**

1. **Container health** — Must show `(healthy)`, NOT `(unhealthy)`
   ```bash
   docker ps | grep bukidapp
   ```
2. **If unhealthy**, check app logs:
   ```bash
   docker logs <container-name> --tail 100
   ```
3. **Most common cause**: Redis or MySQL DNS resolution failure → wrong hostname in env vars
4. **Network**: Verify container is on `dokploy-network`:
   ```bash
   docker network inspect dokploy-network --format '{{range .Containers}}{{.Name}} {{end}}' | tr ' ' '\n' | grep bukid
   ```
5. **Redeploy**: Domain/label changes require a full **Redeploy** in Dokploy UI, not just restart

### Redis DNS Lookup Failed

```
RedisException: DNS Lookup resolve failed
```

- **Cause**: Wrong `REDIS_HOST` value
- **Fix**: Use full container name → `docker ps --format '{{.Names}}' | grep dragonfly`

### MySQL Access Denied

```
SQLSTATE[HY000] [1044] Access denied for user 'mysql'@'%' to database 'bukid'
```

- **Cause**: Database doesn't exist or user lacks permissions
- **Fix**: Create the database and grant permissions (see [Step 5](#step-5-create-the-database))

### Traefik Logs Empty

```bash
docker logs dokploy-traefik --tail 50
```

If empty, Traefik hasn't encountered errors — the issue is likely the container being unhealthy.

### Container Keeps Restarting

Check logs for startup errors:
```bash
docker logs <container-name> --tail 200
```

Common causes:
- Missing `.env` variables
- Database not reachable
- Redis not reachable
- PHP extension missing

---

## Reference: Working Configuration Files

### `docker-compose.yml` (committed in repo root)

```yaml
services:
  bukidapp:
    build:
      context: .
      dockerfile: Dockerfile.php
    ports:
      - 9501
    networks:
      - dokploy-network
    healthcheck:
      test: [ "CMD-SHELL", "curl -f http://localhost:9501/health || exit 1" ]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s

networks:
  dokploy-network:
    external: true
```

### `Dockerfile.php`

```dockerfile
FROM hyperf/hyperf:8.3-alpine-v3.19-swoole-v6

USER root

RUN apk add --no-cache \
    postgresql-dev \
    php83-pdo_pgsql \
    php83-pgsql \
    7zip

RUN apk add --no-cache curl \
    && curl -fsSL https://unofficial-builds.nodejs.org/download/release/v20.19.0/node-v20.19.0-linux-x64-musl.tar.gz \
    | tar -xz -C /usr/local --strip-components=1 \
    && node -v \
    && npm -v

RUN echo "extension=pdo_pgsql.so" > /etc/php83/conf.d/01_pdo_pgsql.ini && \
    echo "extension=pgsql.so" > /etc/php83/conf.d/00_pgsql.ini

WORKDIR /var/app

COPY package*.json ./
RUN npm install

COPY . .
RUN npm run build

EXPOSE 9501

COPY composer.json composer.lock ./
RUN composer install --no-dev --optimize-autoloader

CMD ["php", "artisan", "serve"]
```

### Required Environment Variables

```env
# Database
DB_CONNECTION=mysql
DB_HOST=<mysql-swarm-service-name>
DB_DATABASE=bukid
DB_PORT=3306
DB_USERNAME=mysql
DB_PASSWORD=<password>

# Redis / DragonflyDB
CACHE_DRIVER=redis
REDIS_HOST=<dragonflydb-container-name>
REDIS_AUTH=<password>
REDIS_PORT=6379
REDIS_DB=0
```

### `/health` Route (in `routes/web.php`)

```php
Route::get('/health', function () {
    return 'OK';
});
```

---

## Quick Reference: Useful Commands

All commands are run on the **Hostinger server terminal** unless otherwise noted.

```bash
# List all running containers
docker ps

# Find a specific container name
docker ps --format '{{.Names}}' | grep <keyword>

# List Swarm services (for Dokploy Databases)
docker service ls

# Check container health
docker ps | grep <container>

# View container logs
docker logs <container-name> --tail 100

# Inspect dokploy-network's connected containers
docker network inspect dokploy-network --format '{{range .Containers}}{{.Name}} {{end}}'

# Test connectivity (run in Dokploy container terminal via UI)
ping -c 2 <target-hostname>

# Run artisan commands (via Dokploy container terminal via UI)
php artisan migrate
php artisan db:seed

# Connect to MySQL (via Hostinger server terminal)
docker exec -it $(docker ps --format '{{.Names}}' | grep prodsqlmain) mysql -u root -p
```
