A personal single-board Kanban task manager with multi-channel reminders.
- Backend: Laravel 12.x, PHP 8.4
- Database: PostgreSQL 17
- API: PostgREST (direct browser access via Caddy reverse proxy)
- Frontend: Bootstrap 5.3, jQuery 3.7.1, jQuery UI 1.13.2 Sortable, jQuery UI Touch Punch, Summernote 0.9, Flatpickr, SweetAlert2
- Web Server: Caddy (reverse proxy with TLS)
- Containerization: Docker + Docker Compose
You need Docker and Docker Compose. No local PHP or PostgreSQL required — everything runs in containers.
git clone https://github.com/btafoya/phpkanmaster
cd phpkanmaster
cp .env.example .envThe app uses a single user defined in .env. Edit these lines:
APP_USER=admin
APP_PASSWORD_HASH=$2y$12$... # Generate with: docker compose exec app php -r "echo password_hash('your_password', PASSWORD_BCRYPT);"You also need to set JWT_SECRET for PostgREST authentication:
JWT_SECRET=your_secret_at_least_32_charactersdocker compose up -d --buildThis builds all images and starts seven services: app (PHP-FPM), db (PostgreSQL 17), postgrest (REST API), sse (real-time updates), caddy (reverse proxy), scheduler (Laravel scheduler for reminders), and backup (daily pg_dump). The database schema is created by Laravel migrations.
The Dockerfile does not bundle vendor dependencies. Run Composer inside the container:
docker compose exec -w /var/www/html app composer update
docker compose exec -w /var/www/html scheduler composer update
docker compose exec -w /app sse composer updatedocker compose exec app php artisan key:generate --showCopy the output into .env:
APP_KEY=base64:...docker compose exec app php artisan migratehttp://localhost:8181/login
Log in with the username and password you set in step 2. The HTTPS port is 8443 if you configure TLS in docker/caddy/Caddyfile.
Run the included script instead of manual steps:
./install.shThe script prompts for username and password, starts the stack, installs dependencies, generates the app key and password hash, and runs migrations.
| Service | Container | Description |
|---|---|---|
app |
phpkanmaster_app |
PHP 8.4-FPM running Laravel |
db |
phpkanmaster_db |
PostgreSQL 17 database (exposed on host port 5436) |
postgrest |
phpkanmaster_postgrest |
PostgREST API (internal only, no exposed port) |
sse |
phpkanmaster_sse |
SSE server for real-time board updates (internal only) |
caddy |
phpkanmaster_caddy |
Reverse proxy (host ports 8181/8443) |
scheduler |
phpkanmaster_scheduler |
Laravel scheduler (schedule:work) for reminders |
backup |
phpkanmaster_backup |
Daily PostgreSQL backup (cron at 2 AM, 7-day retention) |
# Start all services (build if needed)
docker compose up -d --build
# Stop all services
docker compose down
# Stop and remove volumes (resets database)
docker compose down -v
# Restart a single service
docker compose restart sse# Rebuild and restart a specific service
docker compose up -d --build sse
# Rebuild all services
docker compose up -d --builddocker compose exec -w /var/www/html app composer update
docker compose exec -w /var/www/html scheduler composer update
docker compose exec -w /app sse composer updatedocker compose exec app php artisan migrate
docker compose exec app php artisan migrate --force # production
docker compose exec app php artisan cache:clear
docker compose exec app php artisan key:generate --show
docker compose exec app php artisan tinker# psql shell inside the db container
docker compose exec db psql -U kanban -d kanban
# Connect from host via exposed port 5436
psql -h localhost -p 5436 -U kanban -d kanban
# Generate a bcrypt password hash for .env
docker compose exec app php -r "echo password_hash('your_password', PASSWORD_BCRYPT);"npm run dev # dev mode with HMR
npm run build # production buildnpm test # JS unit tests (Vitest)
npm run test:phpunit # PHPUnit tests (in-memory SQLite, no Docker needed)
npm run test:phpstan # PHPStan static analysis
npm run test:all # all three above, in sequence# All container logs
docker compose logs -f
# Per-service logs
docker compose logs -f app
docker compose logs -f scheduler
docker compose logs -f sse
docker compose logs -f caddy
docker compose logs -f db
docker compose logs -f postgrest
docker compose logs -f backup
# Application logs (rotated daily)
docker compose exec app cat storage/logs/laravel-$(date +%Y-%m-%d).logBackups run automatically daily at 2 AM via the backup service. Backups are stored in ./backups/ with retention of 7 days.
# Start the backup service
docker compose up -d --build backup
# Manually trigger a backup
docker compose exec backup /usr/local/bin/backup.sh
# Restore from backup
gunzip < backups/kanban_YYYYMMDD_HHMMSS.sql.gz | docker compose exec -T db psql -U kanban -d kanbanEdit .env:
APP_USER=admin
APP_PASSWORD_HASH=$2y$12$... # bcrypt hash
JWT_SECRET=your_secret_at_least_32_charactersEnable channels in .env and set the respective tokens:
NOTIFY_PUSHOVER=false
NOTIFY_TWILIO=false
NOTIFY_ROCKETCHAT=falsePushover:
PUSHOVER_TOKEN=your_pushover_token
PUSHOVER_USER_KEY=your_user_keyTwilio:
TWILIO_ACCOUNT_SID=your_account_sid
TWILIO_AUTH_TOKEN=your_auth_token
TWILIO_FROM=+15550000000RocketChat:
ROCKETCHAT_URL=https://your-rocket-chat.example.com
ROCKETCHAT_TOKEN=your_bot_token
ROCKETCHAT_CHANNEL=#generalBrowser → Caddy :80
/api/agent/token → PHP-FPM (Laravel) app:9000
/api/* → strip prefix → PostgREST:3000
/sse → SSE server sse:8080 (real-time board updates)
/* → PHP-FPM (Laravel) app:9000
docker/caddy/Caddyfile defines this routing. PostgREST is internal-only (no exposed port).
Laravel uses a custom single-user auth driver (no user table):
app/Auth/SingleUser.php— implementsAuthenticatableapp/Auth/SingleUserProvider.php— validates againstAPP_USER/APP_PASSWORD_HASHfrom.envapp/Providers/SingleUserAuthServiceProvider.php— registers thesingle-userdriverconfig/auth.php— definessingleguard usingsingle-userprovider
Guard name is auth:single (used in routes/web.php).
resources/views/kanban.blade.php is a single Blade view that loads CDN assets and mounts public/assets/js/app.js. The JS is a plain static file, not built by Vite.
window.POSTGREST_URL is set inline from env('PGRST_BASE_URL', '/api') so the JS knows the API base URL.
The board also connects to /sse via EventSource for real-time updates — when tasks, categories, files, or notes change, PostgreSQL pg_notify triggers push events through the SSE server to all connected browsers.
Tables are managed by Laravel migrations in database/migrations/:
tasks— columns:id(uuid),title,description,task_column(enum: new/in_progress/review/on_hold/done),priority(low/medium/high),position,category_id,parent_id(for subtasks),due_date,reminder_at,reminder_sent,pushover_*fieldstask_notes—id,task_id,content(notes), timestampsrecurrence_rules—id,task_id,rrule,next_occurrence_at,end_date,activecategories—id,name,colortask_files—id,task_id,filename,content(base64)
PostgREST connects as kanban_postgrest user with anon role having CRUD permissions on these tables.
Laravel's own tables (users, cache, jobs, sessions) are also created by migrations.
- Docker and Docker Compose
- A domain name pointed to your server (optional, for TLS)
dockeranddocker composecommands available
1. Copy the production environment template:
cp .env.production.example .env2. Set your credentials in .env:
APP_KEY= # Run: docker compose exec app php artisan key:generate
APP_DEBUG=false
APP_URL=https://your-domain.com
APP_PASSWORD_HASH= # Run: docker compose exec app php -r "echo password_hash('your_password', PASSWORD_BCRYPT);"
JWT_SECRET= # Strong random string, 32+ characters3. Set database passwords in docker-compose.yml:
In the db service, set POSTGRES_PASSWORD to a strong random value.
In the postgrest service, set POSTGREST_PASSWORD environment variable.
Set JWT_SECRET in the postgrest service.
4. Start the stack:
docker compose up -d --build5. Run migrations:
docker compose exec app php artisan migrate --force6. Caddy handles TLS automatically — as long as port 80 and 443 are open, Let's Encrypt certificates are provisioned on first request.
| Variable | Value | Why |
|---|---|---|
APP_ENV |
production |
Disables debug mode |
APP_DEBUG |
false |
Hides stack traces |
LOG_STACK |
daily |
Rotates logs daily, keeps 14 days |
LOG_LEVEL |
warning |
Reduces log noise |
SESSION_ENCRYPT |
true |
Encrypts session cookies |
- Login page accessible at
/login - Unauthenticated
/redirects to login - Authenticated users see kanban board
- Logout invalidates session
MIT