# Fresh Ubuntu VPS Deployment for nyone.app This guide explains how to deploy Nyone from only the `docker-bundle/` folder on a fresh Ubuntu VPS. It assumes: - App domain: `nyone.app` - HLS playback domain: `hls.nyone.app` - RTMP ingest URL: `rtmp://nyone.app:1935` - Docker Apache is private on `127.0.0.1:8080` - Docker MediaMTX HLS is private on `127.0.0.1:8888` - 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 `docker-bundle/runtime/source`. ## 1. Point DNS to the VPS Create DNS records before requesting SSL certificates: ```text nyone.app A hls.nyone.app 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.app dig +short hls.nyone.app ``` ## 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 unzip zip ``` ## 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`, MediaMTX HLS is bound to `127.0.0.1:8888`, and 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/docker-bundle/ ``` Back on the VPS: ```bash cd /opt/nyone/docker-bundle cp .env.example .env chmod +x scripts/deploy.sh chmod +x scripts/backup.sh ``` ## 6. Configure docker-bundle/.env Edit the Docker deployment environment: ```bash nano /opt/nyone/docker-bundle/.env ``` Use these values for `nyone.app`: ```env COMPOSE_PROJECT_NAME=nyone GIT_REPOSITORY_URL=git@github.com:your-org/nyone.git GIT_REF=main PUBLIC_SCHEME=https APP_DOMAIN=nyone.app HLS_DOMAIN=hls.nyone.app APP_HTTP_PORT=127.0.0.1:8080 HLS_HTTP_PORT=127.0.0.1:8888 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/docker-bundle ./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.app hls.nyone.app; 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.app \ -d hls.nyone.app ``` The certificate files should be under: ```text /etc/letsencrypt/live/nyone.app/fullchain.pem /etc/letsencrypt/live/nyone.app/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.app hls.nyone.app; 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.app; ssl_certificate /etc/letsencrypt/live/nyone.app/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/nyone.app/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.app; ssl_certificate /etc/letsencrypt/live/nyone.app/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/nyone.app/privkey.pem; location / { if ($request_method = OPTIONS) { return 204; } proxy_pass http://127.0.0.1:8888; 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 Access-Control-Allow-Origin "https://nyone.app" always; add_header Access-Control-Allow-Methods "GET, HEAD, OPTIONS" always; add_header Access-Control-Allow-Headers "Range, Origin, Accept, Content-Type" always; add_header Access-Control-Expose-Headers "Content-Length, Content-Range" always; 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.app/up ``` Check container status: ```bash cd /opt/nyone/docker-bundle 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 HLS CORS headers: ```bash curl -I -H "Origin: https://nyone.app" "https://hls.nyone.app//index.m3u8" ``` The response should include exactly one `Access-Control-Allow-Origin: https://nyone.app` header. A `404` can be normal when the channel is not currently publishing, but duplicate `Access-Control-Allow-Origin` headers mean both Nginx and an upstream are setting CORS; rebuild the MediaMTX container from this bundle and reload Nginx. 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.app:1935 OBS stream key: ?token= ``` Viewer playback URLs generated by Laravel should look like: ```text https://hls.nyone.app//index.m3u8 ``` ## 14. Redeploy after app changes When the Git branch changes: ```bash cd /opt/nyone/docker-bundle ./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 ``` ## 15. Create a restore-ready backup Run backups from the docker-bundle deployment directory: ```bash cd /opt/nyone/docker-bundle ./scripts/backup.sh ``` For the safest storage and recording snapshot, use a maintenance window and stop writer services while the backup runs: ```bash ./scripts/backup.sh --stop-writers ``` The final archive is written to: ```text /opt/nyone/docker-bundle/backups/nyone-backup-YYYYMMDD-HHMMSS.zip ``` Copy that one zip file off the VPS. It contains the docker-bundle deployment files, `.env`, source checkout, PostgreSQL dump, app storage, Redis data, checksums, restore notes, and a restore helper. Restore on a fresh Ubuntu VPS after installing Docker and copying the zip: ```bash unzip nyone-backup-YYYYMMDD-HHMMSS.zip cd nyone-backup-YYYYMMDD-HHMMSS chmod +x restore.sh ./restore.sh /opt/nyone/docker-bundle ``` If `/opt/nyone/docker-bundle` already exists and you intentionally want to replace it: ```bash ./restore.sh --replace /opt/nyone/docker-bundle ``` Host-level Nginx files and Let's Encrypt certificates live outside `docker-bundle/`, so recreate or back them up separately. ## 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