Files
sides/docs/03-DEPLOYMENT.md
root c1b2a8d553 docs: rewrite deployment guide for Docker-only, add reverse proxy section
- Remove adminer service (not used with PostgreSQL)
- Rewrite 03-DEPLOYMENT.md: Docker Compose is the only supported method
- Add reverse proxy examples (Nginx, Caddy, Cloudflare Tunnel)
- Document trusted proxy configuration for Laravel
- Add production checklist and autoscript env var documentation
- Remove all Makefile references (not recommended)
2026-05-21 02:46:41 +08:00

7.5 KiB

Deployment & Infrastructure

Architecture Overview

Internet → Reverse Proxy (443) → nginx container (80) → php-fpm container (9000) → postgres container (5432)

SIDES is deployed exclusively via Docker Compose. The stack consists of 4 services:

Service Image Purpose
app Custom (php:8.2-fpm) Laravel application (PHP-FPM)
postgres postgres:16 PostgreSQL database
web nginx:stable-alpine Reverse proxy to PHP-FPM, serves static assets
pgadmin dpage/pgadmin4 Database management UI (optional)

Prerequisites

  • Docker Engine ^20.x
  • Docker Compose v2+
  • A reverse proxy in front (Nginx, Caddy, Traefik, Cloudflare Tunnel, etc.) for TLS termination

Environment Configuration

Two .env files control the stack:

Root .env — Docker Compose variables

Located at the project root. Controls container-level settings and credential defaults.

POSTGRES_DB="sides_db"
POSTGRES_USER="tck"
POSTGRES_PASSWORD="<your_strong_password>"

PGADMIN_EMAIL="admin@example.com"
PGADMIN_PASSWORD="<your_pgadmin_password>"

FIREBASE_PROJECT_ID="sides-b4abb"
FIREBASE_CREDENTIALS="storage/app/firebase/sides-b4abb-3604a7cf7584.json"

FCM_TOPIC_RAINFALL_WARNING="rainfall_warning"
FCM_TOPIC_RAINFALL_DANGER="rainfall_danger"
FCM_TOPIC_WATERLEVEL_ALERT="waterlevel_alert"
FCM_TOPIC_WATERLEVEL_DANGER="waterlevel_danger"

ADMIN_EMAIL="admin@example.com"
ADMIN_PASSWORD="<set_a_strong_password>"

FTP_SERVER="myvscada.com"
FTP_USERNAME="tck"
FTP_PASSWORD="<your_ftp_password>"

Application .env — Laravel variables

Located at src/.env. This file references Docker service names (e.g. DB_HOST=postgres) so Laravel connects to containers, not external hosts.

Key settings:

APP_NAME=SIDES
APP_ENV=production
APP_DEBUG=false
APP_URL=https://sides.tck.com.my

DB_CONNECTION=pgsql
DB_HOST=postgres
DB_PORT=5432
DB_DATABASE=${POSTGRES_DB}
DB_USERNAME=${POSTGRES_USER}
DB_PASSWORD=${POSTGRES_PASSWORD}

SESSION_DRIVER=database
CACHE_STORE=database
QUEUE_CONNECTION=database

Quick Start

# 1. Clone the repository
git clone <repo-url> && cd tckdev

# 2. Configure environment
cp .env.example .env
# Edit .env with your actual passwords and settings

# 3. Configure Laravel env
cp src/.env.example src/.env
# Edit src/.env — ensure DB_HOST=postgres (the Docker service name)

# 4. Build and start
docker compose up -d --build

The app container's entrypoint automatically runs on first start:

  1. composer install (if vendor/ missing)
  2. npm install && npm run build (if public/build/ missing)
  3. php artisan migrate --force (if RUN_MIGRATIONS=true)
  4. php artisan db:seed --force (if RUN_SEEDER=true)
  5. Caches config, routes, and views
  6. Starts PHP-FPM

Common Operations

docker compose ps                    # Check container status
docker compose logs app              # View Laravel logs
docker compose logs app --follow     # Tail Laravel logs
docker compose logs postgres         # View database logs

docker compose exec app bash         # Shell into app container
docker compose exec app php artisan migrate           # Run migrations
docker compose exec app php artisan migrate:fresh --seed  # Reset DB
docker compose exec app php artisan db:seed            # Run seeders
docker compose exec app php artisan tinker             # Interactive REPL
docker compose exec app php artisan test               # Run tests

docker compose down                  # Stop all containers
docker compose down --volumes        # Stop and delete data volumes
docker compose up -d --build         # Rebuild and restart

Reverse Proxy Setup

The web container (nginx) listens on port 80 internally. A reverse proxy in front handles TLS termination and forwards traffic to it.

The nginx container does not handle HTTPS itself — it expects plain HTTP from the reverse proxy. Configure your reverse proxy to:

  1. Listen on port 443 with your TLS certificate
  2. Forward to localhost:80 (or the Docker host's port 80)
  3. Set the X-Forwarded-Proto header so Laravel generates https:// URLs
  4. Set the X-Forwarded-Host header with the original hostname

Example: Nginx Reverse Proxy

On the host machine running Docker, add a server block:

server {
    listen 443 ssl http2;
    server_name sides.tck.com.my;

    ssl_certificate     /etc/ssl/certs/sides.tck.com.my.crt;
    ssl_certificate_key /etc/ssl/private/sides.tck.com.my.key;

    location / {
        proxy_pass http://127.0.0.1:80;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Example: Caddy

sides.tck.com.my {
    reverse_proxy localhost:80
}

Caddy handles TLS automatically via Let's Encrypt.

Example: Cloudflare Tunnel

cloudflared tunnel --hostname sides.tck.com.my --url http://localhost:80

Laravel Trusted Proxy

Laravel must trust the proxy headers to generate correct URLs. The app already has URL::forceScheme('https') enabled in AppServiceProvider. Ensure APP_URL in src/.env uses https://:

APP_URL=https://sides.tck.com.my

If using a load balancer or multi-hop proxy, configure TrustProxies middleware:

// src/app/Http/Middleware/TrustProxies.php
protected $proxies = '*';
protected $headers = Request::HEADER_X_FORWARDED_FOR |
                     Request::HEADER_X_FORWARDED_HOST |
                     Request::HEADER_X_FORWARDED_PORT |
                     Request::HEADER_X_FORWARDED_PROTO;

Access Points

Service Internal External (via reverse proxy)
Application web:80 https://sides.tck.com.my
pgAdmin pgadmin:80 http://<host>:5050
PostgreSQL postgres:5432 localhost:5432 (optional, for external tools)

Security note: pgAdmin (port 5050) and PostgreSQL (port 5432) should not be exposed to the public internet. Restrict access via firewall rules or only publish to 127.0.0.1:

# docker-compose.yml — restrict to localhost only
ports:
  - "127.0.0.1:5050:80"    # pgAdmin
  - "127.0.0.1:5432:5432"  # PostgreSQL

Data Persistence

Two named volumes store persistent data:

Volume Mount Point Purpose
pgdata /var/lib/postgresql/data PostgreSQL data
pgadmin_data /var/lib/pgadmin pgAdmin sessions and config

The src/ directory is bind-mounted into both app and web containers at /var/www/html.

Autoscript Deployment

The Python data pipeline (autoscript/sidesdecode.py) connects to PostgreSQL via environment variables. When running it outside Docker (e.g. as a cron job on the host):

export PG_HOST=localhost
export PG_PORT=5432
export PG_DATABASE=sides_db
export PG_USER=tck
export PG_PASSWORD=<your_password>
export FTP_SERVER=myvscada.com
export FTP_USERNAME=tck
export FTP_PASSWORD=<your_ftp_password>

python3 autoscript/sidesdecode.py

When running inside Docker on the same network, use PG_HOST=postgres (the service name).

Production Checklist

  • APP_ENV=production and APP_DEBUG=false in src/.env
  • APP_URL uses https://
  • All passwords in .env changed from defaults
  • ADMIN_PASSWORD set to a strong value in root .env
  • TLS configured on reverse proxy
  • pgAdmin and PostgreSQL ports restricted to localhost
  • Firebase credentials JSON stored securely (not in git)
  • Mail credentials configured for password reset