12 KiB
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:
nyone.app A <VPS_PUBLIC_IPV4>
hls.nyone.app A <VPS_PUBLIC_IPV4>
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:
dig +short nyone.app
dig +short hls.nyone.app
2. SSH into the VPS
ssh root@<VPS_PUBLIC_IPV4>
Update the server:
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:
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:
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:
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 <<EOF
Types: deb
URIs: https://download.docker.com/linux/ubuntu
Suites: $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}")
Components: stable
Architectures: $(dpkg --print-architecture)
Signed-By: /etc/apt/keyrings/docker.asc
EOF
apt-get update
apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
systemctl enable --now docker
docker run hello-world
5. Copy only the docker-bundle folder to the server
Create the deployment directory on the VPS:
mkdir -p /opt/nyone
From your local machine, copy the folder:
rsync -av --exclude '.env' --exclude 'runtime' docker-bundle/ root@<VPS_PUBLIC_IPV4>:/opt/nyone/docker-bundle/
Back on the VPS:
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:
nano /opt/nyone/docker-bundle/.env
Use these values for nyone.app:
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:
APP_KEY=
POSTGRES_PASSWORD=
MEDIAMTX_SHARED_SECRET=
If the repository is public, you can use an HTTPS clone URL instead:
GIT_REPOSITORY_URL=https://github.com/your-org/nyone.git
If GIT_REPOSITORY_URL is private, install a deploy key on the VPS:
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:
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:
ssh -T git@github.com
7. Run the Docker deployment
cd /opt/nyone/docker-bundle
./scripts/deploy.sh
The script will clone the app into runtime/source, build the images, run migrations, and start:
appdbredismediamtxqueueschedulerviewer-sync
Check status:
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:
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:
mkdir -p /var/www/certbot
9. Create temporary Nginx config for SSL issuance
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:
certbot certonly --webroot \
-w /var/www/certbot \
-d nyone.app \
-d hls.nyone.app
The certificate files should be under:
/etc/letsencrypt/live/nyone.app/fullchain.pem
/etc/letsencrypt/live/nyone.app/privkey.pem
10. Install final Nginx proxy config
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
certbot renew --dry-run
12. Verify the deployment
Check the app health endpoint:
curl -I https://nyone.app/up
Check container status:
cd /opt/nyone/docker-bundle
docker compose --env-file .env -f docker-compose.yml ps
Check database migrations:
docker compose --env-file .env -f docker-compose.yml exec app php artisan migrate:status
Run one viewer-count sync pass:
docker compose --env-file .env -f docker-compose.yml exec app php artisan streaming:sync-viewer-counts --once -v
Check HLS CORS headers:
curl -I -H "Origin: https://nyone.app" "https://hls.nyone.app/<channel-slug>/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:
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:
OBS server: rtmp://nyone.app:1935
OBS stream key: <channel-slug>?token=<generated-stream-key>
Viewer playback URLs generated by Laravel should look like:
https://hls.nyone.app/<channel-slug>/index.m3u8
14. Redeploy after app changes
When the Git branch changes:
cd /opt/nyone/docker-bundle
./scripts/deploy.sh
Useful logs:
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:
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:
./scripts/backup.sh --stop-writers
The final archive is written to:
/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:
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:
./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