diff --git a/dockerized/UBUNTU_VPS_NYONE_APP_DEPLOYMENT.md b/dockerized/UBUNTU_VPS_NYONE_APP_DEPLOYMENT.md new file mode 100644 index 0000000..b3097c7 --- /dev/null +++ b/dockerized/UBUNTU_VPS_NYONE_APP_DEPLOYMENT.md @@ -0,0 +1,433 @@ +# Fresh Ubuntu VPS Deployment for nyone.net + +This guide explains how to deploy Nyone from only the `dockerized/` folder on a fresh Ubuntu VPS. + +It assumes: + +- App domain: `nyone.net` +- HLS playback domain: `hls.nyone.net` +- RTMP ingest URL: `rtmp://nyone.net:1935` +- Docker Apache is private on `127.0.0.1:8080` +- Host Nginx handles public HTTP, HTTPS, and SSL certificates + +The application repository itself does not need to exist on the server before deployment. The deploy script clones it into `dockerized/runtime/source`. + +## 1. Point DNS to the VPS + +Create DNS records before requesting SSL certificates: + +```text +nyone.net A +hls.nyone.net A +``` + +If your DNS provider has an HTTP proxy mode, make sure TCP `1935` still reaches the VPS directly for RTMP. With Cloudflare, that usually means the RTMP hostname must be DNS-only, not proxied. + +Wait until DNS resolves from the VPS or your local machine: + +```bash +dig +short nyone.net +dig +short hls.nyone.net +``` + +## 2. SSH into the VPS + +```bash +ssh root@ +``` + +Update the server: + +```bash +apt-get update +apt-get upgrade -y +apt-get install -y ca-certificates curl git gnupg lsb-release nginx openssl snapd ufw +``` + +## 3. Configure the firewall + +Allow SSH, HTTP/HTTPS, and RTMP: + +```bash +ufw allow OpenSSH +ufw allow 'Nginx Full' +ufw allow 1935/tcp +ufw --force enable +ufw status +``` + +Docker can publish container ports outside some UFW rules. In this deployment, Apache is bound to `127.0.0.1:8080`, while RTMP is intentionally public on `1935`. + +## 4. Install Docker Engine and Compose plugin + +Remove conflicting packages if they exist: + +```bash +for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do + apt-get remove -y "$pkg" || true +done +``` + +Install Docker from Docker's official Ubuntu apt repository: + +```bash +apt-get update +apt-get install -y ca-certificates curl +install -m 0755 -d /etc/apt/keyrings +curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc +chmod a+r /etc/apt/keyrings/docker.asc + +tee /etc/apt/sources.list.d/docker.sources >/dev/null <:/opt/nyone/dockerized/ +``` + +Back on the VPS: + +```bash +cd /opt/nyone/dockerized +cp .env.example .env +chmod +x scripts/deploy.sh +``` + +## 6. Configure dockerized/.env + +Edit the Docker deployment environment: + +```bash +nano /opt/nyone/dockerized/.env +``` + +Use these values for `nyone.net`: + +```env +COMPOSE_PROJECT_NAME=nyone + +GIT_REPOSITORY_URL=git@github.com:your-org/nyone.git +GIT_REF=main + +PUBLIC_SCHEME=https +APP_DOMAIN=nyone.net +HLS_DOMAIN=hls.nyone.net +APP_HTTP_PORT=127.0.0.1:8080 +RTMP_PUBLIC_PORT=1935 + +APP_NAME=Nyone +APP_ENV=production +APP_DEBUG=false + +POSTGRES_DB=nyone +POSTGRES_USER=nyone + +SEED_DATABASE=false +APP_OPTIMIZE_ON_BOOT=true +QUEUE_NAMES=default +``` + +Leave these empty on first deploy if you want the deploy script to generate them: + +```env +APP_KEY= +POSTGRES_PASSWORD= +MEDIAMTX_SHARED_SECRET= +``` + +If the repository is public, you can use an HTTPS clone URL instead: + +```env +GIT_REPOSITORY_URL=https://github.com/your-org/nyone.git +``` + +If `GIT_REPOSITORY_URL` is private, install a deploy key on the VPS: + +```bash +ssh-keygen -t ed25519 -C "nyone-vps-deploy" -f ~/.ssh/nyone_deploy +cat ~/.ssh/nyone_deploy.pub +``` + +Add the printed public key as a read-only deploy key in the Git provider, then configure SSH: + +```bash +cat >> ~/.ssh/config <<'EOF' +Host github.com + HostName github.com + User git + IdentityFile ~/.ssh/nyone_deploy + IdentitiesOnly yes +EOF + +chmod 600 ~/.ssh/config ~/.ssh/nyone_deploy +``` + +Confirm Git access: + +```bash +ssh -T git@github.com +``` + +## 7. Run the Docker deployment + +```bash +cd /opt/nyone/dockerized +./scripts/deploy.sh +``` + +The script will clone the app into `runtime/source`, build the images, run migrations, and start: + +- `app` +- `db` +- `redis` +- `mediamtx` +- `queue` +- `scheduler` +- `viewer-sync` + +Check status: + +```bash +docker compose --env-file .env -f docker-compose.yml ps +docker compose --env-file .env -f docker-compose.yml logs --tail=100 app +docker compose --env-file .env -f docker-compose.yml logs --tail=100 mediamtx +``` + +## 8. Install Certbot + +Install Certbot through snap: + +```bash +snap install core +snap refresh core +apt-get remove -y certbot || true +snap install --classic certbot +ln -sf /snap/bin/certbot /usr/local/bin/certbot +``` + +Create the webroot used for certificate challenges: + +```bash +mkdir -p /var/www/certbot +``` + +## 9. Create temporary Nginx config for SSL issuance + +```bash +tee /etc/nginx/sites-available/nyone-temp.conf >/dev/null <<'EOF' +server { + listen 80; + listen [::]:80; + server_name nyone.net hls.nyone.net; + + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + location / { + return 200 "nyone certificate setup\n"; + add_header Content-Type text/plain; + } +} +EOF + +ln -sf /etc/nginx/sites-available/nyone-temp.conf /etc/nginx/sites-enabled/nyone-temp.conf +rm -f /etc/nginx/sites-enabled/default +nginx -t +systemctl reload nginx +``` + +Request the certificate: + +```bash +certbot certonly --webroot \ + -w /var/www/certbot \ + -d nyone.net \ + -d hls.nyone.net +``` + +The certificate files should be under: + +```text +/etc/letsencrypt/live/nyone.net/fullchain.pem +/etc/letsencrypt/live/nyone.net/privkey.pem +``` + +## 10. Install final Nginx proxy config + +```bash +tee /etc/nginx/sites-available/nyone.conf >/dev/null <<'EOF' +server { + listen 80; + listen [::]:80; + server_name nyone.net hls.nyone.net; + + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + location / { + return 301 https://$host$request_uri; + } +} + +server { + listen 443 ssl http2; + listen [::]:443 ssl http2; + server_name nyone.net; + + ssl_certificate /etc/letsencrypt/live/nyone.net/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/nyone.net/privkey.pem; + + client_max_body_size 100M; + + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + + location / { + proxy_pass http://127.0.0.1:8080; + 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-Host $host; + proxy_set_header X-Forwarded-Port 443; + proxy_set_header X-Forwarded-Proto https; + proxy_read_timeout 120s; + } +} + +server { + listen 443 ssl http2; + listen [::]:443 ssl http2; + server_name hls.nyone.net; + + ssl_certificate /etc/letsencrypt/live/nyone.net/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/nyone.net/privkey.pem; + + location / { + proxy_pass http://127.0.0.1:8080; + 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-Host $host; + proxy_set_header X-Forwarded-Port 443; + proxy_set_header X-Forwarded-Proto https; + proxy_buffering off; + proxy_request_buffering off; + proxy_read_timeout 60s; + proxy_send_timeout 60s; + add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate" always; + } +} +EOF + +ln -sf /etc/nginx/sites-available/nyone.conf /etc/nginx/sites-enabled/nyone.conf +rm -f /etc/nginx/sites-enabled/nyone-temp.conf +nginx -t +systemctl reload nginx +``` + +## 11. Test certificate renewal + +```bash +certbot renew --dry-run +``` + +## 12. Verify the deployment + +Check the app health endpoint: + +```bash +curl -I https://nyone.net/up +``` + +Check container status: + +```bash +cd /opt/nyone/dockerized +docker compose --env-file .env -f docker-compose.yml ps +``` + +Check database migrations: + +```bash +docker compose --env-file .env -f docker-compose.yml exec app php artisan migrate:status +``` + +Run one viewer-count sync pass: + +```bash +docker compose --env-file .env -f docker-compose.yml exec app php artisan streaming:sync-viewer-counts --once -v +``` + +Check that MediaMTX API is not exposed on the host. This should fail because the API is only available inside the Docker network: + +```bash +curl -I http://127.0.0.1:9997/v3/config/get +``` + +Do not expose port `9997` on the public firewall. + +## 13. OBS and playback values + +Creators should use: + +```text +OBS server: rtmp://nyone.net:1935 +OBS stream key: ?token= +``` + +Viewer playback URLs generated by Laravel should look like: + +```text +https://hls.nyone.net//index.m3u8 +``` + +## 14. Redeploy after app changes + +When the Git branch changes: + +```bash +cd /opt/nyone/dockerized +./scripts/deploy.sh +``` + +Useful logs: + +```bash +docker compose --env-file .env -f docker-compose.yml logs -f app +docker compose --env-file .env -f docker-compose.yml logs -f queue +docker compose --env-file .env -f docker-compose.yml logs -f scheduler +docker compose --env-file .env -f docker-compose.yml logs -f viewer-sync +docker compose --env-file .env -f docker-compose.yml logs -f mediamtx +``` + +## References + +- Docker Engine on Ubuntu: https://docs.docker.com/engine/install/ubuntu/ +- Certbot Nginx instructions: https://certbot.eff.org/instructions +- Laravel trusted proxy behavior: https://laravel.com/docs/13.x/requests#configuring-trusted-proxies