Monorepo with:
- Frontend: Next.js app in
front_end vibecode dnl/dnl_front_end/ - Backend: Express/MySQL API in
vibe code dnl/
For a fresh Docker setup that initializes a new MySQL volume from db/dispatch_todo_app.sql, the seeded admin login is:
- Username:
admin - Password:
admin
Important:
- this only applies to a fresh database initialized from the seed SQL
- if the MySQL volume already exists, Docker will keep the existing data instead of re-importing the seed
- change the admin password after first login in any shared or non-local environment
Runs frontend lint/build/audit, starts next start, probes a couple pages, then does backend audit, starts the API, and probes auth behavior.
powershell -ExecutionPolicy Bypass -File scripts\smoke.ps1Useful options:
# Use different ports
powershell -ExecutionPolicy Bypass -File scripts\smoke.ps1 -FrontendPort 3011 -BackendPort 5011
# Skip expensive steps
powershell -ExecutionPolicy Bypass -File scripts\smoke.ps1 -SkipAudit -SkipBuild
# Run only one side
powershell -ExecutionPolicy Bypass -File scripts\smoke.ps1 -SkipBackend
powershell -ExecutionPolicy Bypass -File scripts\smoke.ps1 -SkipFrontendFrontend:
cd "front_end vibecode dnl\dnl_front_end"
npm install
npm run devBackend:
cd "vibe code dnl"
npm install
$env:PORT=5000
npm run devRecommended production target: one VPS running Docker with Nginx on the host.
Architecture:
- Frontend container listens on internal port
3000 - Backend container listens on internal port
5000 - MySQL runs in Docker with a persistent named volume
- Uploaded files live in a persistent named volume
- Nginx on the VPS terminates TLS and reverse-proxies by domain/subdomain
Recommended domains:
dispatch.example.com-> frontendapi.dispatch.example.com-> backend
Important:
- Do not use
docker-compose.local.ymlon the VPS. - Do not expose MySQL publicly.
- Do not bind the app publicly to
3000or5000. - If another app already uses VPS port
3000, that is fine. Keep this app behind Nginx on localhost-only container bindings.
git clone https://github.com/level-top/dnl-dispatch.git
cd dnl-dispatchcp .env.example .envUpdate .env with real values:
MYSQL_DATABASE=dispatch_todo_app
MYSQL_USER=dnl
MYSQL_PASSWORD=use-a-strong-password
MYSQL_ROOT_PASSWORD=use-a-different-strong-root-password
JWT_SECRET=use-a-long-random-secret
CORS_ORIGIN=https://dispatch.example.com
SCREENSHOT_ALLOWED_HOSTS=dispatch.example.com,api.dispatch.example.com
NEXT_PUBLIC_API_BASE=https://api.dispatch.example.com/apiCreate docker-compose.vps.yml:
services:
backend:
ports:
- "127.0.0.1:5001:5000"
frontend:
ports:
- "127.0.0.1:3001:3000"This keeps the containers private while allowing host Nginx to proxy to them.
docker compose -f docker-compose.yml -f docker-compose.vps.yml up -d --buildCheck status:
docker compose -f docker-compose.yml -f docker-compose.vps.yml ps
docker compose -f docker-compose.yml -f docker-compose.vps.yml logs -f backend
docker compose -f docker-compose.yml -f docker-compose.vps.yml logs -f frontend
docker compose -f docker-compose.yml -f docker-compose.vps.yml logs -f dbExample Nginx config:
server {
listen 80;
server_name dispatch.example.com;
location / {
proxy_pass http://127.0.0.1:3001;
proxy_http_version 1.1;
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;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
server {
listen 80;
server_name api.dispatch.example.com;
location / {
proxy_pass http://127.0.0.1:5001;
proxy_http_version 1.1;
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;
}
}After DNS points to the VPS:
sudo certbot --nginx -d dispatch.example.com -d api.dispatch.example.comcurl -I https://dispatch.example.com
curl -I https://api.dispatch.example.com/Expected result:
- frontend returns
200 - backend root returns
200 - protected API routes return
401without a token
git pull
docker compose -f docker-compose.yml -f docker-compose.vps.yml up -d --buildIf you want every push to main to deploy automatically to the VPS, this repo includes a workflow at .github/workflows/deploy.yml.
What it does:
- connects to your VPS over SSH
- runs
scripts/deploy-vps.sh - pulls the latest
main - rebuilds and restarts the Docker stack with
docker-compose.vps.yml
Server preparation:
cd /var/www/dnl-dispatch
chmod +x scripts/deploy-vps.shImportant:
- the VPS deploy user must have access to Docker without interactive sudo
- the repo on the VPS must already be cloned at
/var/www/dnl-dispatch docker-compose.vps.ymland.envmust already exist on the VPS
Add these GitHub repository secrets:
VPS_HOST: your server IP or hostnameVPS_USER: the Linux user used for deploymentVPS_SSH_KEY: the private SSH key for that userVPS_PORT: optional, defaults to22VPS_APP_DIR: optional, defaults to/var/www/dnl-dispatch
Then every push to main will deploy automatically. You can also run it manually from the GitHub Actions tab with workflow_dispatch.
Production notes:
- The MySQL container initializes from the SQL files in
db/only on the first boot of a fresh database volume. - Uploaded files are stored in the
uploads_datavolume and survive container rebuilds. - The backend screenshot route uses Chromium inside the container.
- Back up both
mysql_dataanduploads_data.
The repo now includes a daily MySQL backup workflow that writes compressed dumps into backups/daily/ on the host. That host folder is mounted into the backend container as /app/backups, which allows admin-only API access to the saved dumps.
From the repo root on the server:
bash ./scripts/db-backup.shWhat it does:
- creates
backups/daily/if it does not exist - runs
mysqldumpinside the runningdbcontainer - stores a file like
dnl-backup-2026-05-18_02-00-00.sql.gz - deletes backup files older than
BACKUP_RETENTION_DAYSdays
On a Linux VPS:
bash ./scripts/install-daily-backup-cron.shDefault schedule:
- every day at
02:00
Optional environment variables:
CRON_SCHEDULE="0 2 * * *"
BACKUP_RETENTION_DAYS=30These endpoints require a valid admin JWT:
GET /api/backups: list available backup filesGET /api/backups/:fileName/download: download one backup fileDELETE /api/backups/:fileName: remove one backup file from the server
Example with curl:
curl -H "Authorization: Bearer YOUR_ADMIN_TOKEN" https://api.dispatch.example.com/api/backups
curl -L -H "Authorization: Bearer YOUR_ADMIN_TOKEN" -o latest-backup.sql.gz https://api.dispatch.example.com/api/backups/dnl-backup-2026-05-18_02-00-00.sql.gz/download
curl -X DELETE -H "Authorization: Bearer YOUR_ADMIN_TOKEN" https://api.dispatch.example.com/api/backups/dnl-backup-2026-05-18_02-00-00.sql.gzImportant:
- keep backups offsite as well if the VPS disk fails
- test restore periodically instead of assuming dumps are valid
- do not commit files from
backups/daily/
PostgreSQL is possible, but it is a separate migration, not a container-only change. The backend currently depends on MySQL-specific behavior and query syntax in many places, including:
mysql2/promiseconnection handlingINSERT IGNOREON DUPLICATE KEY UPDATEIFNULL(...)DATE_ADD(..., INTERVAL 1 DAY)- MySQL placeholder and bulk insert patterns such as
VALUES ?
Recommendation:
- Deploy with Docker on MySQL first.
- Migrate to PostgreSQL as a separate refactor once production deployment is stable.
- Next.js 16 requires Node.js >= 20.9.0. For deployment/CI, using an LTS Node version is recommended.
- If you see
EADDRINUSE, pick different ports (or stop the process already listening on that port).