2026-02-28 21:43:44 +00:00
# ALWISP – Mesh Network Solutions
> Dockerized LAMP stack website for Alabama's wireless ISP and mesh networking company.
---
## Table of Contents
- [Stack Overview ](#stack-overview )
- [Installation – Unraid ](#installation--unraid )
- [Environment Variables ](#environment-variables )
- [Project Structure ](#project-structure )
- [Roadmap & Milestones ](#roadmap--milestones )
- [Updating the Site ](#updating-the-site )
- [Useful Commands ](#useful-commands )
---
## Stack Overview
| Container | Image | Purpose |
|---|---|---|
| `alwisp_web` | PHP 8.2 + Apache | Serves the website |
| `alwisp_db` | MySQL 8.0 | Database (internal only) |
| `alwisp_pma` | phpMyAdmin (optional) | DB admin UI on port 8080 |
2026-02-28 22:10:32 +00:00
Each container is deployed individually through the Unraid Container Builder and connected via the `br0` bridge network, giving both the web container and the database container their own dedicated LAN IP addresses. The web container reaches the database by its fixed LAN IP. Data persists in a named Docker volume (`db_data` ) and survives container restarts and rebuilds.
2026-02-28 21:43:44 +00:00
---
## Installation – Unraid
### Prerequisites
- Unraid 6.10 or later
- **Community Applications** plugin installed
2026-02-28 22:10:32 +00:00
- **Docker** enabled (Settings → Docker → Enable Docker: Yes)
2026-02-28 21:43:44 +00:00
- **User Scripts** plugin (recommended, for scheduled tasks)
2026-02-28 22:10:32 +00:00
- `br0` enabled — your Unraid host NIC must be bridged (Settings → Network → Enable Bridging: Yes)
- Decide on **two free LAN IPs ** before you start — one for the web container, one for the database. Example: `192.168.1.100` (web) and `192.168.1.101` (db). Reserve these in your router's DHCP settings so they are never auto-assigned.
2026-02-28 21:43:44 +00:00
---
2026-02-28 22:38:51 +00:00
### Step 1 – Clone the repository onto your Unraid cache drive
> **Unraid volume mount requirement:** Docker containers on Unraid **cannot reliably access `/mnt/user/` paths**. `/mnt/user` is a FUSE filesystem that is not available inside the Docker container namespace — the container sees an empty directory, Apache finds no files, and returns a 403 Forbidden. Always clone and mount from the **cache drive path directly** (`/mnt/cache/appdata/`).
2026-02-28 21:43:44 +00:00
Open an Unraid terminal (**Tools → Terminal** or SSH in):
```bash
2026-02-28 22:38:51 +00:00
cd /mnt/cache/appdata
2026-02-28 21:43:44 +00:00
git clone https://github.com/jasonMPM/alwisp.git
cd alwisp
```
---
2026-02-28 22:10:32 +00:00
### Step 2 – Build the Docker image
The web container uses a custom image built from the included `Dockerfile` . Build it once from the terminal — Unraid's Container Builder will reference it by name.
2026-02-28 21:43:44 +00:00
```bash
2026-02-28 22:38:51 +00:00
cd /mnt/cache/appdata/alwisp
2026-02-28 22:10:32 +00:00
docker build -t alwisp_web:latest .
2026-02-28 21:43:44 +00:00
```
2026-02-28 22:10:32 +00:00
> After any code change to the `Dockerfile`, re-run this command and then restart the container from the Docker tab.
2026-02-28 21:43:44 +00:00
2026-02-28 22:10:32 +00:00
---
### Step 3 – Add the Database container (alwisp_db)
1. In the Unraid web UI, go to the **Docker ** tab
2. Click **Add Container **
3. Fill in the form:
2026-02-28 21:43:44 +00:00
2026-02-28 22:10:32 +00:00
| Field | Value |
|---|---|
| **Name ** | `alwisp_db` |
| **Repository ** | `mysql:8.0` |
| **Network Type ** | `br0` |
| **Fixed IP ** | `192.168.1.101` * (your reserved DB IP) * |
| **Restart Policy ** | `Unless Stopped` |
4. Scroll to **Environment Variables ** and add:
| Key | Value |
|---|---|
| `MYSQL_ROOT_PASSWORD` | * (strong root password) * |
| `MYSQL_DATABASE` | `alwisp` |
| `MYSQL_USER` | `alwisp_user` |
| `MYSQL_PASSWORD` | * (strong db password) * |
5. Scroll to **Path/Volume Mappings ** and add:
| Container Path | Host Path | Access Mode |
|---|---|---|
2026-02-28 22:38:51 +00:00
| `/var/lib/mysql` | `/mnt/cache/appdata/alwisp/db_data` | Read/Write |
| `/docker-entrypoint-initdb.d/init.sql` | `/mnt/cache/appdata/alwisp/docker/mysql/init.sql` | Read Only |
2026-02-28 21:43:44 +00:00
2026-02-28 22:10:32 +00:00
6. Click **Apply ** — Unraid pulls the MySQL image and starts the container
2026-02-28 21:43:44 +00:00
---
2026-02-28 22:10:32 +00:00
### Step 4 – Add the Web container (alwisp_web)
2026-02-28 21:43:44 +00:00
2026-02-28 22:10:32 +00:00
1. Click **Add Container ** again
2026-02-28 22:21:35 +00:00
2. Enable **Advanced View ** (toggle in the top-right of the form) so the Extra Parameters field is visible
3. Fill in the form:
2026-02-28 21:43:44 +00:00
2026-02-28 22:10:32 +00:00
| Field | Value |
|---|---|
| **Name ** | `alwisp_web` |
| **Repository ** | `alwisp_web:latest` * (the image you built in Step 2) * |
| **Network Type ** | `br0` |
| **Fixed IP ** | `192.168.1.100` * (your reserved web IP) * |
| **Restart Policy ** | `Unless Stopped` |
2026-02-28 22:21:35 +00:00
| **Extra Parameters ** | `--pull=never` |
> **Why `--pull=never`?** Unraid's Container Builder always tries to pull the repository name from Docker Hub before starting the container. Since `alwisp_web` is a locally built image that doesn't exist on Docker Hub, the pull fails. `--pull=never` tells Docker to use the locally built image as-is and skip the pull attempt.
2026-02-28 21:43:44 +00:00
2026-02-28 22:21:35 +00:00
4. Add **Environment Variables ** :
2026-02-28 22:10:32 +00:00
| Key | Value |
|---|---|
| `DB_HOST` | `192.168.1.101` * (the DB container's br0 IP from Step 3) * |
| `DB_NAME` | `alwisp` |
| `DB_USER` | `alwisp_user` |
| `DB_PASS` | * (same password set in Step 3) * |
4. Add **Path/Volume Mappings ** :
| Container Path | Host Path | Access Mode |
|---|---|---|
2026-02-28 22:38:51 +00:00
| `/var/www/html` | `/mnt/cache/appdata/alwisp/www` | Read/Write |
| `/etc/apache2/ssl` | `/mnt/cache/appdata/alwisp/docker/apache/ssl` | Read Only |
2026-02-28 22:10:32 +00:00
2026-02-28 22:29:18 +00:00
> The Apache vhost config (`000-default.conf`) is baked directly into the image via `COPY` in the Dockerfile — no bind mount needed. To change it, edit the file in `docker/apache/` and rebuild the image.
2026-02-28 22:10:32 +00:00
5. Click **Apply **
2026-02-28 21:43:44 +00:00
---
### Step 5 – Verify containers are running
```bash
docker ps --filter name=alwisp
```
2026-02-28 22:10:32 +00:00
Both `alwisp_web` and `alwisp_db` should show status `Up` .
2026-02-28 21:43:44 +00:00
2026-02-28 22:10:32 +00:00
Navigate to the web container's dedicated IP in a browser:
2026-02-28 21:43:44 +00:00
```
2026-02-28 22:10:32 +00:00
http://192.168.1.100 → website (replace with your web IP)
2026-02-28 21:43:44 +00:00
```
2026-02-28 22:10:32 +00:00
Because `br0` gives the container its own LAN IP, **no port mapping is needed ** — it behaves like a separate device on your network.
2026-02-28 21:43:44 +00:00
2026-02-28 22:10:32 +00:00
---
### Step 6 – Add phpMyAdmin (optional)
For database administration, add a third container:
1. Click **Add Container **
2. Fill in:
| Field | Value |
|---|---|
| **Name ** | `alwisp_pma` |
| **Repository ** | `phpmyadmin:latest` |
| **Network Type ** | `br0` |
| **Fixed IP ** | `192.168.1.102` * (another free LAN IP) * |
3. Add **Environment Variables ** :
| Key | Value |
|---|---|
| `PMA_HOST` | `192.168.1.101` * (DB container's br0 IP) * |
| `PMA_USER` | `alwisp_user` |
| `PMA_PASSWORD` | * (your db password) * |
4. Click **Apply ** , then browse to `http://192.168.1.102`
2026-02-28 21:43:44 +00:00
---
2026-02-28 22:10:32 +00:00
### Step 7 – Point a domain (optional)
2026-02-28 21:43:44 +00:00
2026-02-28 22:10:32 +00:00
Because the web container has a dedicated LAN IP, reverse proxy setup is straightforward:
2026-02-28 21:43:44 +00:00
2026-02-28 22:10:32 +00:00
**Using Nginx Proxy Manager on Unraid:**
1. Add a **Proxy Host ** in Nginx Proxy Manager
2. Forward hostname/IP: `192.168.1.100` (the web container's br0 IP), port `80`
2026-02-28 21:43:44 +00:00
3. Enable SSL via Let's Encrypt
2026-02-28 22:38:51 +00:00
4. Drop your certificate files into `/mnt/cache/appdata/alwisp/docker/apache/ssl/` — they are already mounted into the container
2026-02-28 21:43:44 +00:00
---
### Recommended Unraid Share Settings
| Setting | Value |
|---|---|
2026-02-28 22:38:51 +00:00
| Share path | `/mnt/cache/appdata/alwisp` |
2026-02-28 21:43:44 +00:00
| Use cache | Yes (cache-only or prefer) |
| Exclude from backup | No — include in Appdata backup |
---
## Environment Variables
| Variable | Description |
|---|---|
| `DB_ROOT_PASS` | MySQL root password — make this strong |
| `DB_NAME` | Database name (default: `alwisp` ) |
| `DB_USER` | Application DB user (default: `alwisp_user` ) |
| `DB_PASS` | Application DB password — make this strong |
---
## Project Structure
```
alwisp/
├── docker-compose.yml # Container orchestration
├── Dockerfile # PHP 8.2 + Apache image
├── .env # Secrets — never commit
├── .gitignore
├── docker/
│ ├── apache/
│ │ ├── 000-default.conf # Apache vhost config
│ │ └── ssl/ # TLS certs (gitignored)
│ ├── mysql/
│ │ └── init.sql # Schema bootstrap on first run
│ └── php/
│ └── php.ini # PHP runtime settings
└── www/ # Web root (live-mounted into container)
├── index.php # Front controller / router
├── .htaccess # URL rewriting
├── css/style.css # Design system
├── js/main.js # Nav, counters, scroll reveal
├── assets/ # Logos, images
├── includes/
│ ├── header.php # Global nav
│ └── footer.php # Global footer
└── pages/
├── home.php
├── services.php
├── coverage.php
├── about.php
├── contact.php
└── 404.php
```
---
## Roadmap & Milestones
### Milestone 1 — Foundation `complete`
- [x] Dockerized LAMP stack (PHP 8.2, Apache, MySQL 8.0)
- [x] Front-controller PHP router
- [x] Responsive site skeleton with brand design system
- [x] Homepage: hero, stats bar, services preview, why section, coverage CTA
- [x] Contact form with server-side validation
- [x] Stub pages for all top-level routes (services, coverage, about, contact, 404)
- [x] Security headers, OPcache, and Apache hardening
- [x] Unraid-ready deployment via Docker Compose
---
### Milestone 2 — Content & Plans
- [ ] Populate residential plan tiers (speed tiers, pricing cards)
- [ ] Populate business plan tiers
- [ ] Infrastructure and managed services detail pages
- [ ] About page — company story, team bios, mission statement
- [ ] Real contact info (phone, email, address) wired into header and footer
- [ ] Logo assets added to `/www/assets/` and displayed in nav
---
### Milestone 3 — Coverage Map
- [ ] Embed interactive map (Leaflet.js + OpenStreetMap)
- [ ] Define and store coverage zone polygons in the database
- [ ] Address lookup / service availability check on the homepage CTA
- [ ] Mobile-optimized map view
---
### Milestone 4 — Customer Portal (Phase 1)
- [ ] Customer account creation and login (PHP sessions)
- [ ] Account dashboard — plan details, billing status, support tickets
- [ ] Contact form wired to database and email notification (PHPMailer)
- [ ] Admin panel — view and respond to contact submissions
- [ ] Password reset via email token
---
### Milestone 5 — Billing Integration
- [ ] Stripe payment integration for online bill pay
- [ ] Invoice generation and PDF download
- [ ] Recurring payment setup
- [ ] Payment history view in customer portal
---
### Milestone 6 — Network Status Page
- [ ] Public status page showing uptime and active incidents
- [ ] Admin interface to post and resolve incidents
- [ ] Automated uptime monitoring hook (Uptime Kuma or similar)
- [ ] Email/SMS notification for outages
---
### Milestone 7 — SEO, Performance & Security
- [ ] SSL/TLS configured end-to-end (Let's Encrypt via Nginx Proxy Manager)
- [ ] Sitemap.xml and robots.txt
- [ ] Open Graph and Twitter Card meta tags for social sharing
- [ ] Image optimization pipeline (WebP conversion on upload)
- [ ] Content Security Policy header tuned
- [ ] Security audit and pen test
---
### Milestone 8 — Operations & Automation
- [ ] Automated database backups to Unraid share via User Scripts cron
- [ ] Log rotation configured
- [ ] Staging environment (second compose stack on alternate ports)
- [ ] CI/CD pipeline — auto-deploy on push to `main` via webhook
---
## Updating the Site
2026-02-28 22:38:51 +00:00
Because `/mnt/cache/appdata/alwisp/www` is bind-mounted directly into `alwisp_web` , **PHP and asset changes take effect immediately ** — no rebuild or restart needed.
2026-02-28 21:43:44 +00:00
```bash
# Pull latest code
2026-02-28 22:38:51 +00:00
cd /mnt/cache/appdata/alwisp
2026-02-28 21:43:44 +00:00
git pull origin main
2026-02-28 22:10:32 +00:00
# If the Dockerfile changed, rebuild the image and restart the web container:
2026-02-28 22:38:51 +00:00
docker build -t alwisp_web:latest /mnt/cache/appdata/alwisp
2026-02-28 22:10:32 +00:00
docker restart alwisp_web
2026-02-28 21:43:44 +00:00
```
2026-02-28 22:10:32 +00:00
The database container (`alwisp_db` ) is completely independent — updates to the site never affect it.
2026-02-28 21:43:44 +00:00
---
## Useful Commands
```bash
2026-02-28 22:10:32 +00:00
# Rebuild the web image after Dockerfile changes
2026-02-28 22:38:51 +00:00
docker build -t alwisp_web:latest /mnt/cache/appdata/alwisp
2026-02-28 21:43:44 +00:00
2026-02-28 22:10:32 +00:00
# Start / stop individual containers
docker start alwisp_web
docker stop alwisp_web
docker restart alwisp_web
2026-02-28 21:43:44 +00:00
2026-02-28 22:10:32 +00:00
docker start alwisp_db
docker stop alwisp_db
2026-02-28 21:43:44 +00:00
2026-02-28 22:10:32 +00:00
# View live logs
docker logs -f alwisp_web
docker logs -f alwisp_db
2026-02-28 21:43:44 +00:00
# Open a shell inside the web container
docker exec -it alwisp_web bash
2026-02-28 22:10:32 +00:00
# Open a MySQL shell (use the DB container's br0 IP if connecting externally)
2026-02-28 21:43:44 +00:00
docker exec -it alwisp_db mysql -u alwisp_user -p alwisp
# Manual database backup
2026-02-28 22:38:51 +00:00
docker exec alwisp_db mysqldump -u alwisp_user -p alwisp > /mnt/cache/appdata/alwisp/backups/backup_$(date +%F).sql
2026-02-28 21:43:44 +00:00
```
---
## License
Proprietary — Alabama WISP LLC. All rights reserved.