# Farmers For Forests - Laravel Voyager Admin — Project Setup (Fresh Machine)

Complete walkthrough for getting **Farmers For Forests - Laravel Voyager Admin** running on a brand new machine, from cloning the repo to a working app in the browser. Everything runs inside Docker, so you do **not** need PHP, Composer, or a database server installed locally.

Current stack: **Laravel 10.50.2** · **PHP 8.2 (php:8.2-apache)** · **Composer 2** · **Voyager 1.7** (admin panel) · **MySQL 8.0**.

> PHP is pinned to 8.2 because Voyager 1.7's own `composer.json` caps support at `^8.2` — it does not declare PHP 8.3 support. Don't bump the base image without also re-checking Voyager's compatibility.

For a day-to-day command reference (migrations, cache clearing, generators, Voyager, etc.) see [commands.md](commands.md) once you're set up.

---

## Status

All steps below (1–12) have been completed and verified on this machine:

- Docker images built, all three containers (`fff_laravel_voyager`, `fff_laravel_voyager_db`, `fff_laravel_voyager_phpmyadmin`) up and healthy
- `.env` created with `APP_KEY` generated
- All migrations ran successfully (including Voyager's own migrations and the app's custom tables — teams, partners, programs, projects, project metrics, news, jobs, dispatches, home content, contact us, etc.)
- `voyager:install` completed — `Voyager::routes()` wired into `routes/web.php` under the `admin` prefix, `App\Models\User` extends `TCG\Voyager\Models\User`
- `public/storage` symlink created and verified (`LINKED`)
- `db:seed` run — populates Voyager's core admin data **and** all of the app's own public-site content (CMS pages, teams, partners, programs, projects, blog, news, jobs, home content) via `database/seeders/DatabaseSeeder.php`
- Admin user created and confirmed working
- App reachable at `http://localhost:8080` (renders the actual Farmers For Forests homepage, not Laravel's default welcome page — the `/` route is wired to `PageController::index`), admin panel at `http://localhost:8080/admin`, phpMyAdmin at `http://localhost:8081`

If you're setting this up fresh on a new machine, just follow steps 1–12 in order — they're written for that from-scratch case.

---

## 1. Prerequisites

You only need two things installed on the host machine:

| Tool | Check with | Install if missing |
|---|---|---|
| **Git** | `git --version` | macOS: `xcode-select --install`. Linux: your package manager (`apt install git`, etc). Windows: [git-scm.com](https://git-scm.com/download/win) |
| **Docker Desktop** (with Docker Compose) | `docker --version` && `docker compose version` | [docker.com/get-started](https://www.docker.com/get-started/) — make sure Docker Desktop is running before continuing |

You do **not** need PHP, Composer, or Node installed locally — the Docker image provides PHP 8.2 + Composer, and `docker-compose.yml` also spins up a MySQL 8.0 container for the database.

Verify Docker is actually running:

```
docker info
```

If this errors, start Docker Desktop and wait for it to finish booting before continuing.

---

## 2. Clone the repository

```
git clone git@gitlab.com:farmers-for-forests/farmers-for-forests-laravel-voyager.git fff_laravel_voyager
cd fff_laravel_voyager
```

(Skip this if you already have the repo — just `cd` into it.)

---

## 3. Create your `.env` file

The repo ships a `.env.example` template. Copy it to `.env`:

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

Default `.env` values already work out of the box for local development against the bundled `db` container:

- `APP_NAME="Farmers For Forests - Laravel Voyager Admin"`
- `DB_CONNECTION=mysql`
- `DB_HOST=db` — the name of the MySQL service in `docker-compose.yml`, resolvable from inside the app container
- `DB_DATABASE=fff`
- `DB_USERNAME=root`
- `DB_PASSWORD=root`
- `APP_URL=http://localhost:8080` — **must include the port** the app is actually exposed on. Laravel builds every `asset()`/`Storage::url()` link (including Media Manager and BREAD image fields) from this value — if it's missing the port, uploaded images/files will 404 or show as broken in the browser even though the files exist and the storage symlink is correct. Update this if you change the exposed port below.

`APP_KEY` will be generated in step 6.

`docker-compose.yml` reads `DB_DATABASE`/`DB_PASSWORD` straight out of this `.env` file to seed the MySQL container's `MYSQL_DATABASE`/`MYSQL_ROOT_PASSWORD` on first boot — there's a single source of truth, no need to keep credentials in sync in two places. If you change `DB_DATABASE`/`DB_USERNAME`/`DB_PASSWORD` **after** the `db` container has already initialized its data volume, the change won't retroactively apply (MySQL only reads these on first init) — see the troubleshooting section below.

**Any `.env` change requires recreating the app container to take effect** — `env_file` bakes `.env`'s values into the container at creation time, so Laravel's live-mounted copy of `.env` won't override them. Run `docker compose up -d fff_laravel_voyager` (not `docker compose restart`, which reuses the existing container's already-baked env) after editing `.env`.

---

## 4. Build the Docker image

```
docker compose build
```

This builds a `php:8.2-apache` image with:

- Apache `mod_rewrite` enabled, document root pointed at `public/` (Laravel's entry point)
- PHP extensions Laravel + Voyager need: `pdo_mysql`, `mysqli`, `gd`, `zip`, `bcmath`, `exif`, `pcntl`, `intl` (`opcache` ships enabled by default)
- Composer 2 installed
- `composer install` run automatically during the build

First build takes a few minutes (compiling PHP extensions + downloading Composer packages). Subsequent builds are cached and much faster.

---

## 5. Start the containers

```
docker compose up -d
```

Check they're running:

```
docker compose ps
```

You should see three containers:
- `fff_laravel_voyager` — the app, mapped to `0.0.0.0:8080->80/tcp`
- `fff_laravel_voyager_db` — MySQL 8.0, mapped to `0.0.0.0:3306->3306/tcp`
- `fff_laravel_voyager_phpmyadmin` — phpMyAdmin, mapped to `0.0.0.0:8081->80/tcp`, for browsing the database. Visit `http://localhost:8081` — it logs in automatically using the `DB_USERNAME`/`DB_PASSWORD` from your `.env`.

The compose file bind-mounts the repo into the app container (`.:/var/www/html`) so code edits on the host are reflected immediately, while `vendor/` is kept in a separate named volume (`vendor:/var/www/html/vendor`) so it isn't wiped out by the bind mount. MySQL data persists in the `db_data` named volume across restarts.

---

## 6. Generate the application key

If your `.env` doesn't already have an `APP_KEY` set:

```
docker exec -it fff_laravel_voyager php artisan key:generate
```

---

## 7. Run database migrations

Wait a few seconds after `docker compose up -d` for MySQL to finish initializing on a first run, then:

```
docker exec -it fff_laravel_voyager php artisan migrate
```

---

## 8. Install Voyager

If this is a fresh database (no Voyager tables yet), run the official Voyager installer:

```
docker exec -it fff_laravel_voyager php artisan voyager:install
```

Add `--with-dummy` if you also want demo posts/pages/categories seeded:

```
docker exec -it fff_laravel_voyager php artisan voyager:install --with-dummy
```

This publishes Voyager's migrations/seeders/config, runs them, wires `Voyager::routes()` into `routes/web.php`, sets `App\Models\User` to extend `TCG\Voyager\Models\User`, and creates the `public/storage` symlink.

Create your admin user (if you skipped `--with-dummy`, or want an additional admin):

```
docker exec -it fff_laravel_voyager php artisan voyager:admin your@email.com --create
```

You'll be prompted for a name and password.

---

## 9. Seed the application's content

`voyager:install` (step 8) only seeds Voyager's own admin data (BREAD types, menus, roles, permissions) plus, if you passed `--with-dummy`, Voyager's generic demo posts/pages. It does **not** populate this app's own public-facing site content.

The project's `database/seeders/DatabaseSeeder.php` seeds everything the public site needs to render — CMS pages (about, careers, our-technology, agroforestry, etc.), teams, partners, programs, projects + project metrics, blog posts, news, jobs, home page content, custom menus, and settings. Without this step the homepage and most public routes will load with empty/missing content even though migrations and `voyager:install` succeeded.

```
docker exec -it fff_laravel_voyager php artisan db:seed
```

See [commands.md](commands.md#seed) for details on individual seeders and `migrate:fresh --seed`.

---

## 10. Set filesystem permissions

Laravel needs `storage/` and `bootstrap/cache/` to be writable by the web server user (`www-data`):

```
docker exec -it fff_laravel_voyager chown -R www-data:www-data storage bootstrap/cache
docker exec -it fff_laravel_voyager chmod -R 775 storage bootstrap/cache
```

(The Dockerfile already does this at build time, but re-run it if you ever see "permission denied" errors writing to logs/cache/sessions — most commonly after the bind mount overlays the directory with host-owned files.)

---

## 11. Create the storage symlink

`voyager:install` creates this automatically (step 8). If you ever need to recreate it manually:

```
docker exec -it fff_laravel_voyager php artisan storage:link
```

(If you re-run this on an existing setup, Artisan will report `The [public/storage] link already exists.` — that's expected, not an error.)

---

## 12. Verify everything works

```
docker exec -it fff_laravel_voyager php artisan --version
docker exec -it fff_laravel_voyager php artisan about
```

`php artisan about` should show no errors, `public/storage` as `LINKED`, and database driver `mysql`.

Then open the app in a browser:

```
http://localhost:8080
```

You should see the Farmers For Forests homepage (`/` is wired to `PageController::index`, not Laravel's default welcome page) — with real content (hero, programs, teams, etc.) as long as step 9's `db:seed` ran. If the page loads but looks empty/broken, that's the tell that the seed step was skipped. The Voyager admin panel is at:

```
http://localhost:8080/admin
```

Log in with the admin account you created in step 8.

To browse the database directly, open phpMyAdmin:

```
http://localhost:8081
```

It logs in automatically using `DB_USERNAME`/`DB_PASSWORD` from `.env` (`root`/`root` by default).

If you get a blank page or 500 error, check the logs:

```
docker compose logs -f
docker exec -it fff_laravel_voyager tail -f storage/logs/laravel.log
```

---

## Troubleshooting

**Port 8080 already in use** — another process is bound to it. Either stop that process, or change the host port for the `fff_laravel_voyager` service in `docker-compose.yml`:
```yaml
ports:
  - "8082:80"   # use any free host port
```

**Port 8081 already in use** — same idea, but for the `phpmyadmin` service's port mapping.

**Port 3306 already in use** — you likely have a local MySQL already running. Change the host port for the `db` service in `docker-compose.yml` (e.g. `"3307:3306"`); `DB_HOST=db`/`DB_PORT=3306` in `.env` don't need to change since that's the internal container port.

**"Permission denied" writing to storage/cache** — re-run step 10. This usually happens because the bind mount replaces the directory with host-owned files after a fresh `git clone` or `docker compose down -v`.

**Changed PHP extensions / Dockerfile and nothing takes effect** — rebuild without cache:
```
docker compose build --no-cache
docker compose up -d
```

**`vendor/` seems out of sync after switching branches** — the `vendor` named volume can get stale. Reset it:
```
docker compose down -v
docker compose build
docker compose up -d
```

**Changed `DB_DATABASE`/`DB_USERNAME`/`DB_PASSWORD` in `.env` but the app still can't log in / MySQL rejects the new credentials** — MySQL's official image only applies `MYSQL_DATABASE`/`MYSQL_ROOT_PASSWORD` the *first* time its data directory is initialized; changing `.env` afterward has no effect on the already-initialized `db_data` volume. Reset it (this **deletes all database data** — back up first if you need to keep it):
```
docker compose down
docker volume rm fff_laravel_voyager_db_data
docker compose up -d
```

**Uploaded images / Media Manager files show as broken in the browser, even though `public/storage` exists and points at real files** — check `APP_URL` in `.env`. If it's missing the port (e.g. `http://localhost` instead of `http://localhost:8080`), every generated image URL will point at the wrong port and 404. Fix `APP_URL`, then recreate the app container (`docker compose up -d fff_laravel_voyager`) since `.env` changes don't apply without it (see step 3 above).

**Do not bump the PHP base image past 8.2** without first checking Voyager's `composer.json` — v1.7 declares `"php": "^7.3|^7.4|^8.0|^8.1|^8.2"`, so PHP 8.3+ will fail platform checks unless Voyager is upgraded too (v1.8.0 supports PHP 8.3 but requires Laravel 11).

**Need a shell inside the container:**
```
docker exec -it fff_laravel_voyager bash
```

---

## One-liner (after prerequisites + `.env` are in place)

Once Docker is running and `.env` exists, the whole setup collapses to:

```
docker compose build \
  && docker compose up -d \
  && sleep 5 \
  && docker exec -it fff_laravel_voyager php artisan key:generate \
  && docker exec -it fff_laravel_voyager php artisan migrate \
  && docker exec -it fff_laravel_voyager php artisan voyager:install \
  && docker exec -it fff_laravel_voyager php artisan db:seed \
  && docker exec -it fff_laravel_voyager chown -R www-data:www-data storage bootstrap/cache \
  && docker exec -it fff_laravel_voyager chmod -R 775 storage bootstrap/cache
```

Then create an admin user and visit `http://localhost:8080/admin`:

```
docker exec -it fff_laravel_voyager php artisan voyager:admin your@email.com --create
```
