Compare commits
10 Commits
f624150bd8
...
b3bb160b90
| Author | SHA1 | Date | |
|---|---|---|---|
| b3bb160b90 | |||
| cb62a0bae6 | |||
| f302d605d2 | |||
| cdb89d3287 | |||
| da19b6a8a2 | |||
| 81dd742120 | |||
| 34a683b614 | |||
| c06e237cd1 | |||
| 0fb7531e63 | |||
| b3b7ebf8d7 |
@@ -2,7 +2,7 @@ APP_NAME=Nyone
|
||||
APP_ENV=local
|
||||
APP_KEY=
|
||||
APP_DEBUG=true
|
||||
APP_URL=https://nyone.net
|
||||
APP_URL=https://nyone.app
|
||||
|
||||
APP_LOCALE=en
|
||||
APP_FALLBACK_LOCALE=en
|
||||
@@ -64,7 +64,7 @@ AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||
|
||||
VITE_APP_NAME="${APP_NAME}"
|
||||
|
||||
STREAMING_RTMP_INGEST_URL=rtmp://nyone.net:19935
|
||||
STREAMING_RTMP_INGEST_URL=rtmp://nyone.app:19935
|
||||
STREAMING_HLS_PUBLIC_URL=https://nyone-hls.net
|
||||
STREAMING_MEDIAMTX_API_URL=http://127.0.0.1:9997
|
||||
STREAMING_VIEWER_COUNT_CACHE_STORE=redis
|
||||
|
||||
@@ -35,8 +35,8 @@ The project config is aligned to these production-style domains:
|
||||
|
||||
```env
|
||||
APP_NAME=Nyone
|
||||
APP_URL=https://nyone.net
|
||||
STREAMING_RTMP_INGEST_URL=rtmp://nyone.net:19935
|
||||
APP_URL=https://nyone.app
|
||||
STREAMING_RTMP_INGEST_URL=rtmp://nyone.app:19935
|
||||
STREAMING_HLS_PUBLIC_URL=https://nyone-hls.net
|
||||
MEDIAMTX_SHARED_SECRET=change-this-secret
|
||||
```
|
||||
|
||||
@@ -52,7 +52,7 @@ return [
|
||||
|
|
||||
*/
|
||||
|
||||
'url' => env('APP_URL', 'https://nyone.net'),
|
||||
'url' => env('APP_URL', 'https://nyone.app'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
||||
@@ -41,7 +41,7 @@ return [
|
||||
'public' => [
|
||||
'driver' => 'local',
|
||||
'root' => storage_path('app/public'),
|
||||
'url' => rtrim(env('APP_URL', 'https://nyone.net'), '/').'/storage',
|
||||
'url' => rtrim(env('APP_URL', 'https://nyone.app'), '/').'/storage',
|
||||
'visibility' => 'public',
|
||||
'throw' => false,
|
||||
'report' => false,
|
||||
|
||||
@@ -46,7 +46,7 @@ return [
|
||||
'username' => env('MAIL_USERNAME'),
|
||||
'password' => env('MAIL_PASSWORD'),
|
||||
'timeout' => null,
|
||||
'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url((string) env('APP_URL', 'https://nyone.net'), PHP_URL_HOST)),
|
||||
'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url((string) env('APP_URL', 'https://nyone.app'), PHP_URL_HOST)),
|
||||
],
|
||||
|
||||
'ses' => [
|
||||
|
||||
@@ -12,7 +12,7 @@ return [
|
||||
|
|
||||
*/
|
||||
|
||||
'rtmp_ingest_url' => env('STREAMING_RTMP_INGEST_URL', 'rtmp://nyone.net:19935'),
|
||||
'rtmp_ingest_url' => env('STREAMING_RTMP_INGEST_URL', 'rtmp://nyone.app:19935'),
|
||||
'hls_public_url' => env('STREAMING_HLS_PUBLIC_URL', 'https://nyone-hls.net'),
|
||||
|
||||
/*
|
||||
|
||||
@@ -17,7 +17,7 @@ class DatabaseSeeder extends Seeder
|
||||
{
|
||||
PlatformSetting::current();
|
||||
|
||||
$admin = User::query()->firstOrNew(['email' => 'admin@nyone.net']);
|
||||
$admin = User::query()->firstOrNew(['email' => 'admin@nyone.app']);
|
||||
$admin->forceFill([
|
||||
'name' => 'Admin User',
|
||||
'password' => 'password',
|
||||
@@ -26,7 +26,7 @@ class DatabaseSeeder extends Seeder
|
||||
'can_create_channel' => false,
|
||||
])->save();
|
||||
|
||||
$official = User::query()->firstOrNew(['email' => 'chief@nyone.net']);
|
||||
$official = User::query()->firstOrNew(['email' => 'chief@nyone.app']);
|
||||
$official->forceFill([
|
||||
'name' => 'Chief Nyone',
|
||||
'password' => 'password',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
authMethod: http
|
||||
authHTTPAddress: https://nyone.net/internal/mediamtx/auth?secret=change-this-secret
|
||||
authHTTPAddress: https://nyone.app/internal/mediamtx/auth?secret=change-this-secret
|
||||
authHTTPExclude:
|
||||
- action: read
|
||||
- action: playback
|
||||
@@ -17,17 +17,17 @@ hls: yes
|
||||
hlsAddress: :19988
|
||||
hlsVariant: mpegts
|
||||
hlsAllowOrigins:
|
||||
- https://nyone.net
|
||||
- https://nyone.app
|
||||
|
||||
|
||||
pathDefaults:
|
||||
record: yes
|
||||
recordPath: ./storage/app/private/recordings/%path/%Y-%m-%d_%H-%M-%S-%f
|
||||
|
||||
runOnReady: curl -X POST "https://nyone.net/internal/mediamtx/ready?secret=change-this-secret" --data-urlencode "path=$MTX_PATH" --data-urlencode "source_type=$MTX_SOURCE_TYPE" --data-urlencode "source_id=$MTX_SOURCE_ID"
|
||||
runOnReady: curl -X POST "https://nyone.app/internal/mediamtx/ready?secret=change-this-secret" --data-urlencode "path=$MTX_PATH" --data-urlencode "source_type=$MTX_SOURCE_TYPE" --data-urlencode "source_id=$MTX_SOURCE_ID"
|
||||
runOnReadyRestart: no
|
||||
runOnNotReady: curl -X POST "https://nyone.net/internal/mediamtx/not-ready?secret=change-this-secret" --data-urlencode "path=$MTX_PATH" --data-urlencode "source_type=$MTX_SOURCE_TYPE" --data-urlencode "source_id=$MTX_SOURCE_ID"
|
||||
runOnRecordSegmentComplete: curl -X POST "https://nyone.net/internal/mediamtx/recording-complete?secret=change-this-secret" --data-urlencode "path=$MTX_PATH" --data-urlencode "segment_path=$MTX_SEGMENT_PATH" --data-urlencode "duration=$MTX_SEGMENT_DURATION"
|
||||
runOnNotReady: curl -X POST "https://nyone.app/internal/mediamtx/not-ready?secret=change-this-secret" --data-urlencode "path=$MTX_PATH" --data-urlencode "source_type=$MTX_SOURCE_TYPE" --data-urlencode "source_id=$MTX_SOURCE_ID"
|
||||
runOnRecordSegmentComplete: curl -X POST "https://nyone.app/internal/mediamtx/recording-complete?secret=change-this-secret" --data-urlencode "path=$MTX_PATH" --data-urlencode "segment_path=$MTX_SEGMENT_PATH" --data-urlencode "duration=$MTX_SEGMENT_DURATION"
|
||||
|
||||
paths:
|
||||
all_others:
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
These configs match the current project domains:
|
||||
|
||||
- App: `https://nyone.net`
|
||||
- App: `https://nyone.app`
|
||||
- HLS playback: `https://nyone-hls.net`
|
||||
- OBS ingest: `rtmp://nyone.net:19935`
|
||||
- OBS ingest: `rtmp://nyone.app:19935`
|
||||
|
||||
MediaMTX stays private on local ports:
|
||||
|
||||
@@ -29,14 +29,14 @@ sudo systemctl reload nginx
|
||||
|
||||
Make sure DNS points both hostnames to the server:
|
||||
|
||||
- `nyone.net`
|
||||
- `nyone.app`
|
||||
- `nyone-hls.net`
|
||||
|
||||
The Laravel `.env` should use:
|
||||
|
||||
```env
|
||||
APP_URL=https://nyone.net
|
||||
STREAMING_RTMP_INGEST_URL=rtmp://nyone.net:19935
|
||||
APP_URL=https://nyone.app
|
||||
STREAMING_RTMP_INGEST_URL=rtmp://nyone.app:19935
|
||||
STREAMING_HLS_PUBLIC_URL=https://nyone-hls.net
|
||||
MEDIAMTX_SHARED_SECRET=change-this-secret
|
||||
```
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# Laravel application vhost for nyone.net.
|
||||
# Laravel application vhost for nyone.app.
|
||||
# Copy to /etc/nginx/sites-available/nyone-app.conf and symlink into sites-enabled.
|
||||
# Adjust root and fastcgi_pass for your server.
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name nyone.net;
|
||||
server_name nyone.app;
|
||||
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
@@ -13,13 +13,13 @@ server {
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
listen [::]:443 ssl http2;
|
||||
server_name nyone.net;
|
||||
server_name nyone.app;
|
||||
|
||||
root /var/www/nyone/public;
|
||||
index index.php;
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/nyone.net/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/nyone.net/privkey.pem;
|
||||
ssl_certificate /etc/letsencrypt/live/nyone.app/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/nyone.app/privkey.pem;
|
||||
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# HLS reverse proxy for MediaMTX.
|
||||
# Browser origin is https://nyone.net, while playback URLs point here:
|
||||
# Browser origin is https://nyone.app, while playback URLs point here:
|
||||
# https://nyone-hls.net/<channel-slug>/index.m3u8
|
||||
|
||||
server {
|
||||
|
||||
@@ -9,6 +9,7 @@ PUBLIC_SCHEME=https
|
||||
APP_DOMAIN=nyone.example.com
|
||||
HLS_DOMAIN=nyone-hls.example.com
|
||||
APP_HTTP_PORT=80
|
||||
HLS_HTTP_PORT=127.0.0.1:8888
|
||||
RTMP_PUBLIC_PORT=1935
|
||||
|
||||
APP_NAME=Nyone
|
||||
@@ -1,6 +1,6 @@
|
||||
# Nyone Dockerized Deployment
|
||||
# Nyone Docker Bundle Deployment
|
||||
|
||||
This folder is a self-contained deployment bundle. A server only needs the `dockerized/` directory; the application repository is cloned later into `runtime/source` by `scripts/deploy.sh`.
|
||||
This folder is a self-contained deployment bundle. A server only needs the `docker-bundle/` directory; the application repository is cloned later into `runtime/source` by `scripts/deploy.sh`.
|
||||
|
||||
## What It Runs
|
||||
|
||||
@@ -14,33 +14,35 @@ This folder is a self-contained deployment bundle. A server only needs the `dock
|
||||
|
||||
Default ports use the services' normal defaults:
|
||||
|
||||
- HTTP app/HLS proxy: `80`
|
||||
- HTTP app proxy: `80`
|
||||
- RTMP ingest: `1935`
|
||||
- MediaMTX HLS inside Docker: `8888`
|
||||
- MediaMTX HLS on localhost: `8888`
|
||||
- MediaMTX API inside Docker: `9997`
|
||||
- PostgreSQL inside Docker: `5432`
|
||||
- Redis inside Docker: `6379`
|
||||
|
||||
Only HTTP and RTMP are published by default. HLS is served through Apache on `HLS_DOMAIN`, not directly from MediaMTX.
|
||||
HTTP and RTMP are published by default. HLS is published only on `127.0.0.1:8888` by default so host Nginx can proxy `HLS_DOMAIN` directly to MediaMTX without exposing the HLS port publicly.
|
||||
|
||||
## Server Requirements
|
||||
|
||||
- Docker with Compose v2.
|
||||
- Git.
|
||||
- OpenSSL.
|
||||
- Zip.
|
||||
- Network access from the server to the Git repository.
|
||||
- DNS for both `APP_DOMAIN` and `HLS_DOMAIN`.
|
||||
- External TLS termination through a reverse proxy, load balancer, or CDN.
|
||||
|
||||
If a host-level reverse proxy already uses port `80`, set `APP_HTTP_PORT` to another local port and forward both hostnames to that port.
|
||||
If a host-level reverse proxy already uses port `80`, set `APP_HTTP_PORT` to another local port for the app and keep `HLS_HTTP_PORT` on a local MediaMTX port for HLS.
|
||||
|
||||
## First Deployment
|
||||
|
||||
```bash
|
||||
cd dockerized
|
||||
cd docker-bundle
|
||||
cp .env.example .env
|
||||
nano .env
|
||||
chmod +x scripts/deploy.sh
|
||||
chmod +x scripts/backup.sh
|
||||
./scripts/deploy.sh
|
||||
```
|
||||
|
||||
@@ -69,13 +71,14 @@ The deploy script will:
|
||||
|
||||
## Runtime Configuration
|
||||
|
||||
This bundle does not use the cloned repository's `.env` file for production settings. Docker Compose passes configuration from `dockerized/.env` into the containers.
|
||||
This bundle does not use the cloned repository's `.env` file for production settings. Docker Compose passes configuration from `docker-bundle/.env` into the containers.
|
||||
|
||||
Important values:
|
||||
|
||||
- `APP_DOMAIN`: Laravel app hostname.
|
||||
- `HLS_DOMAIN`: HLS playback hostname. Must differ from `APP_DOMAIN`.
|
||||
- `APP_HTTP_PORT`: host port published by Apache, default `80`.
|
||||
- `HLS_HTTP_PORT`: host address published by MediaMTX HLS, default `127.0.0.1:8888`.
|
||||
- `RTMP_PUBLIC_PORT`: host RTMP port, default `1935`.
|
||||
- `PUBLIC_SCHEME`: usually `https` behind external TLS.
|
||||
- `SEED_DATABASE`: defaults to `false`; set to `true` only if you intentionally want the app seeder to run.
|
||||
@@ -85,7 +88,7 @@ The MediaMTX config is Docker-owned at `docker/mediamtx/mediamtx.yml.template`;
|
||||
|
||||
## Operations
|
||||
|
||||
Run commands from the `dockerized/` directory.
|
||||
Run commands from the `docker-bundle/` directory.
|
||||
|
||||
```bash
|
||||
docker compose --env-file .env -f docker-compose.yml ps
|
||||
@@ -101,6 +104,29 @@ Redeploy after new Git changes:
|
||||
./scripts/deploy.sh
|
||||
```
|
||||
|
||||
Create a restore-ready backup zip:
|
||||
|
||||
```bash
|
||||
./scripts/backup.sh
|
||||
```
|
||||
|
||||
For the most consistent app storage and recording backup, run it during a maintenance window and let the script stop writer services while it archives files:
|
||||
|
||||
```bash
|
||||
./scripts/backup.sh --stop-writers
|
||||
```
|
||||
|
||||
Backups are written to `backups/` by default. Each zip contains deployment files, `.env`, `runtime/source`, PostgreSQL dump, app storage, Redis data, checksums, restore notes, and a `restore.sh` helper.
|
||||
|
||||
Restore on a fresh server after installing Docker and Compose:
|
||||
|
||||
```bash
|
||||
unzip nyone-backup-YYYYMMDD-HHMMSS.zip
|
||||
cd nyone-backup-YYYYMMDD-HHMMSS
|
||||
chmod +x restore.sh
|
||||
./restore.sh /opt/nyone/docker-bundle
|
||||
```
|
||||
|
||||
Stop the stack:
|
||||
|
||||
```bash
|
||||
@@ -111,7 +137,7 @@ Do not remove volumes unless you intend to delete database, Redis, and stored ap
|
||||
|
||||
## External Proxy Notes
|
||||
|
||||
Forward both `APP_DOMAIN` and `HLS_DOMAIN` to the Apache container's published HTTP port. Preserve the `Host` header and set `X-Forwarded-Proto: https` when TLS is terminated outside Docker.
|
||||
Forward `APP_DOMAIN` to the Apache container's published HTTP port. Forward `HLS_DOMAIN` directly to the MediaMTX HLS localhost port. Preserve the `Host` header for the Laravel app and set `X-Forwarded-Proto: https` when TLS is terminated outside Docker.
|
||||
|
||||
Expose TCP `1935` publicly for OBS/RTMP ingest. Do not expose MediaMTX API port `9997` publicly.
|
||||
|
||||
@@ -121,16 +147,17 @@ Use this pattern when Nginx runs on the host and handles public HTTP/HTTPS. In t
|
||||
|
||||
```env
|
||||
APP_HTTP_PORT=127.0.0.1:8080
|
||||
HLS_HTTP_PORT=127.0.0.1:8888
|
||||
PUBLIC_SCHEME=https
|
||||
APP_DOMAIN=app.example.com
|
||||
HLS_DOMAIN=hls.example.com
|
||||
RTMP_PUBLIC_PORT=1935
|
||||
```
|
||||
|
||||
Run `./scripts/deploy.sh` after changing `.env`, then point Nginx at `http://127.0.0.1:8080`. Replace the example domains and certificate paths before enabling the config.
|
||||
Run `./scripts/deploy.sh` after changing `.env`, then point Nginx app traffic at `http://127.0.0.1:8080` and HLS traffic at `http://127.0.0.1:8888`. Replace the example domains and certificate paths before enabling the config.
|
||||
|
||||
```nginx
|
||||
# /etc/nginx/sites-available/nyone-dockerized.conf
|
||||
# /etc/nginx/sites-available/nyone-docker-bundle.conf
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
@@ -178,7 +205,11 @@ server {
|
||||
ssl_certificate_key /etc/letsencrypt/live/hls.example.com/privkey.pem;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:8080;
|
||||
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;
|
||||
@@ -190,6 +221,10 @@ server {
|
||||
proxy_request_buffering off;
|
||||
proxy_read_timeout 60s;
|
||||
proxy_send_timeout 60s;
|
||||
add_header Access-Control-Allow-Origin "https://app.example.com" 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;
|
||||
}
|
||||
}
|
||||
@@ -198,9 +233,21 @@ server {
|
||||
Enable and validate on the server:
|
||||
|
||||
```bash
|
||||
sudo ln -s /etc/nginx/sites-available/nyone-dockerized.conf /etc/nginx/sites-enabled/nyone-dockerized.conf
|
||||
sudo ln -s /etc/nginx/sites-available/nyone-docker-bundle.conf /etc/nginx/sites-enabled/nyone-docker-bundle.conf
|
||||
sudo nginx -t
|
||||
sudo systemctl reload nginx
|
||||
```
|
||||
|
||||
Keep RTMP separate from this HTTP proxy. Open TCP `1935` to the Docker host so OBS can publish directly to MediaMTX.
|
||||
|
||||
## HLS CORS Check
|
||||
|
||||
Host Nginx owns the public HLS CORS headers. MediaMTX is configured with `hlsAllowOrigins: []` so the response does not include duplicate `Access-Control-Allow-Origin` headers after proxying.
|
||||
|
||||
Check the deployed headers with the exact app origin:
|
||||
|
||||
```bash
|
||||
curl -I -H "Origin: https://app.example.com" "https://hls.example.com/channel/index.m3u8"
|
||||
```
|
||||
|
||||
The response should contain one `Access-Control-Allow-Origin` header matching the app origin. If two `Access-Control-Allow-Origin` headers appear, rebuild the MediaMTX container from this bundle and reload Nginx.
|
||||
79
docker-bundle/SKILL.md
Normal file
79
docker-bundle/SKILL.md
Normal file
@@ -0,0 +1,79 @@
|
||||
---
|
||||
name: docker-bundle-deployment
|
||||
description: Work on the Nyone docker-bundle deployment bundle under docker-bundle/. Use when modifying Docker Compose, Apache, MediaMTX, PostgreSQL, Redis, Laravel queue/scheduler/viewer-sync services, deploy.sh, deployment README files, or runtime environment defaults for this self-contained Docker deployment.
|
||||
---
|
||||
|
||||
# Nyone docker-bundle Deployment
|
||||
|
||||
## Core Rules
|
||||
|
||||
- Keep deployment changes inside `docker-bundle/` unless the user explicitly requests application code changes.
|
||||
- Treat `docker-bundle/.env` and `docker-bundle/runtime/` as server-local generated state; do not commit or depend on them.
|
||||
- Do not make this bundle depend on the cloned app repository's `.env` or `deploy/mediamtx.yml`.
|
||||
- Keep MediaMTX config Docker-owned at `docker/mediamtx/mediamtx.yml.template`.
|
||||
- Keep `APP_DOMAIN` and `HLS_DOMAIN` separate because the app and HLS are served by different host-level Nginx virtual hosts.
|
||||
- Do not run `scripts/deploy.sh`, `docker compose up`, or image builds unless the user confirms this is a deployment server.
|
||||
|
||||
## Architecture
|
||||
|
||||
- `scripts/deploy.sh` is the entrypoint for server deployment.
|
||||
- `scripts/backup.sh` creates restore-ready zip backups for the docker-bundle deployment.
|
||||
- `docker-compose.yml` defines `app`, `db`, `redis`, `mediamtx`, `queue`, `scheduler`, `viewer-sync`, and one-shot `migrate`.
|
||||
- `docker/app/Dockerfile` builds the Laravel Apache image from `runtime/source`.
|
||||
- `docker/app/entrypoint.sh` selects behavior by `CONTAINER_ROLE`.
|
||||
- `docker/app/apache/nyone.conf.template` serves Laravel on `APP_DOMAIN`.
|
||||
- Host Nginx should proxy `HLS_DOMAIN` directly to the locally published MediaMTX HLS port.
|
||||
- `docker/mediamtx/entrypoint.sh` renders the MediaMTX template at container start.
|
||||
|
||||
## Port Defaults
|
||||
|
||||
- Apache published HTTP: `80`.
|
||||
- MediaMTX RTMP: `1935`.
|
||||
- MediaMTX HLS local publish/internal: `8888`.
|
||||
- MediaMTX API internal: `9997`.
|
||||
- PostgreSQL internal: `5432`.
|
||||
- Redis internal: `6379`.
|
||||
|
||||
Publish Apache HTTP and RTMP by default, and bind MediaMTX HLS to `127.0.0.1:8888` for host Nginx. Keep the MediaMTX API private on the Docker network.
|
||||
|
||||
## Editing Guidance
|
||||
|
||||
- When adding an environment option, update `.env.example`, `docker-compose.yml`, and any entrypoint/template that consumes it.
|
||||
- When changing MediaMTX ports, update Compose environment, published ports, entrypoint defaults, and the template together.
|
||||
- When changing Laravel runtime behavior, update the shared `x-app-environment` block unless the value belongs to one role only.
|
||||
- When changing queue behavior, prefer env-driven options on the `queue` service and `docker/app/entrypoint.sh`.
|
||||
- Do not run the long-lived Docker `viewer-sync` service with `--isolated`; a stale cache lock can make the command exit cleanly and trigger an endless container restart loop.
|
||||
- Keep `npm run build` after PHP setup and Composer install in `docker/app/Dockerfile`; the Wayfinder Vite plugin invokes `php artisan wayfinder:generate` during the build. The asset stage must also provide safe build-time Laravel env values, create Laravel writable directories such as `storage/framework/views`, and run `php artisan wayfinder:generate --with-form --no-interaction` before `npm run build` so failures are visible in Docker logs.
|
||||
- Do not call `php artisan storage:link --force` as `www-data` during container startup; `public/storage` lives in the root-owned image filesystem. Create the symlink as root in the entrypoint if it is missing.
|
||||
- Keep deploy script behavior idempotent: repeat runs should update the Git checkout, rebuild, migrate, and restart services without deleting volumes.
|
||||
- Keep backup behavior restore-ready: include deployment files, `.env`, source checkout when present, PostgreSQL dump, app storage volume, Redis volume, checksums, restore notes, and a restore helper in one zip.
|
||||
- Keep generated secrets in `.env`; do not bake secrets into images or templates.
|
||||
|
||||
## Nginx TLS Proxy Guidance
|
||||
|
||||
- Document Nginx as a host-level TLS reverse proxy in `README.md`; do not add Nginx as another Compose service unless the user asks.
|
||||
- Keep Docker Apache as the upstream for the app hostname only. Nginx should proxy HLS directly to the locally published MediaMTX HLS port.
|
||||
- Recommend `APP_HTTP_PORT=127.0.0.1:8080` when Nginx owns host ports `80` and `443`.
|
||||
- Recommend `HLS_HTTP_PORT=127.0.0.1:8888` so HLS is reachable by host Nginx but not publicly exposed as a raw port.
|
||||
- Include `X-Forwarded-Proto https`, `X-Forwarded-Port 443`, `X-Forwarded-Host`, `X-Real-IP`, and `X-Forwarded-For` in proxy examples.
|
||||
- Let host Nginx own public HLS CORS headers. Keep `hlsAllowOrigins: []` in the MediaMTX template so proxied HLS responses do not contain duplicate `Access-Control-Allow-Origin` values.
|
||||
- Keep HLS proxy examples buffering off and cache headers set to no-store.
|
||||
- Keep RTMP outside the HTTP proxy guidance unless explicitly using Nginx stream proxying; the default is direct TCP `1935` to MediaMTX through Docker.
|
||||
|
||||
## Static Verification
|
||||
|
||||
Use static checks when not on a deployment server:
|
||||
|
||||
```bash
|
||||
git diff --check
|
||||
rg "1993[5]|1998[8]|80[8]0|25[2]5|nyone[.]net|nyone-hls[.]net|change-this-secre[t]" docker-bundle -n
|
||||
bash -n docker-bundle/scripts/backup.sh
|
||||
```
|
||||
|
||||
If Docker is available and the user allows non-deploy validation, validate only the Compose config:
|
||||
|
||||
```bash
|
||||
docker compose --env-file docker-bundle/.env.example -f docker-bundle/docker-compose.yml config
|
||||
```
|
||||
|
||||
Do not start containers during local verification unless the user explicitly asks for runtime testing.
|
||||
492
docker-bundle/UBUNTU_VPS_NYONE_APP_DEPLOYMENT.md
Normal file
492
docker-bundle/UBUNTU_VPS_NYONE_APP_DEPLOYMENT.md
Normal file
@@ -0,0 +1,492 @@
|
||||
# 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 <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:
|
||||
|
||||
```bash
|
||||
dig +short nyone.app
|
||||
dig +short hls.nyone.app
|
||||
```
|
||||
|
||||
## 2. SSH into the VPS
|
||||
|
||||
```bash
|
||||
ssh root@<VPS_PUBLIC_IPV4>
|
||||
```
|
||||
|
||||
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 <<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:
|
||||
|
||||
```bash
|
||||
mkdir -p /opt/nyone
|
||||
```
|
||||
|
||||
From your local machine, copy the folder:
|
||||
|
||||
```bash
|
||||
rsync -av --exclude '.env' --exclude 'runtime' docker-bundle/ root@<VPS_PUBLIC_IPV4>:/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/<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:
|
||||
|
||||
```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: <channel-slug>?token=<generated-stream-key>
|
||||
```
|
||||
|
||||
Viewer playback URLs generated by Laravel should look like:
|
||||
|
||||
```text
|
||||
https://hls.nyone.app/<channel-slug>/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
|
||||
@@ -59,7 +59,6 @@ x-app-environment: &app-environment
|
||||
MEDIAMTX_SHARED_SECRET: ${MEDIAMTX_SHARED_SECRET:-}
|
||||
APP_DOMAIN: ${APP_DOMAIN:-nyone.example.com}
|
||||
HLS_DOMAIN: ${HLS_DOMAIN:-nyone-hls.example.com}
|
||||
HLS_INTERNAL_PORT: 8888
|
||||
APP_OPTIMIZE_ON_BOOT: ${APP_OPTIMIZE_ON_BOOT:-true}
|
||||
SEED_DATABASE: ${SEED_DATABASE:-false}
|
||||
|
||||
@@ -142,13 +141,12 @@ services:
|
||||
dockerfile: docker/mediamtx/Dockerfile
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
APP_URL: ${PUBLIC_SCHEME:-https}://${APP_DOMAIN:-nyone.example.com}
|
||||
HLS_PUBLIC_URL: ${PUBLIC_SCHEME:-https}://${HLS_DOMAIN:-nyone-hls.example.com}
|
||||
MEDIAMTX_SHARED_SECRET: ${MEDIAMTX_SHARED_SECRET:-}
|
||||
RTMP_INTERNAL_PORT: 1935
|
||||
HLS_INTERNAL_PORT: 8888
|
||||
ports:
|
||||
- "${RTMP_PUBLIC_PORT:-1935}:1935"
|
||||
- "${HLS_HTTP_PORT:-127.0.0.1:8888}:8888"
|
||||
volumes:
|
||||
- app-storage:/var/www/html/storage
|
||||
depends_on:
|
||||
121
docker-bundle/docker/app/Dockerfile
Normal file
121
docker-bundle/docker/app/Dockerfile
Normal file
@@ -0,0 +1,121 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
FROM php:8.4-apache-bookworm AS php-base
|
||||
|
||||
ENV APACHE_DOCUMENT_ROOT=/var/www/html/public \
|
||||
COMPOSER_ALLOW_SUPERUSER=1
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
curl \
|
||||
gettext-base \
|
||||
git \
|
||||
gosu \
|
||||
libicu-dev \
|
||||
libcurl4-openssl-dev \
|
||||
libonig-dev \
|
||||
libpq-dev \
|
||||
libsqlite3-dev \
|
||||
libzip-dev \
|
||||
netcat-openbsd \
|
||||
unzip \
|
||||
&& docker-php-ext-install -j"$(nproc)" \
|
||||
bcmath \
|
||||
curl \
|
||||
intl \
|
||||
mbstring \
|
||||
opcache \
|
||||
pcntl \
|
||||
pdo_pgsql \
|
||||
pdo_sqlite \
|
||||
pgsql \
|
||||
zip \
|
||||
&& pecl install redis \
|
||||
&& docker-php-ext-enable redis \
|
||||
&& a2enmod headers rewrite setenvif \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
|
||||
|
||||
WORKDIR /var/www/html
|
||||
|
||||
FROM php-base AS composer-deps
|
||||
|
||||
COPY runtime/source/composer.json runtime/source/composer.lock ./
|
||||
RUN composer install --no-dev --no-interaction --prefer-dist --optimize-autoloader --no-scripts
|
||||
|
||||
FROM php-base AS assets
|
||||
|
||||
ENV APP_NAME=Nyone \
|
||||
APP_ENV=production \
|
||||
APP_DEBUG=false \
|
||||
APP_KEY=base64:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA \
|
||||
APP_URL=http://localhost \
|
||||
LOG_CHANNEL=stderr \
|
||||
DB_CONNECTION=sqlite \
|
||||
DB_DATABASE=:memory: \
|
||||
CACHE_STORE=array \
|
||||
SESSION_DRIVER=array \
|
||||
QUEUE_CONNECTION=sync \
|
||||
MAIL_MAILER=log \
|
||||
BROADCAST_CONNECTION=log \
|
||||
FILESYSTEM_DISK=local \
|
||||
VITE_APP_NAME=Nyone \
|
||||
STREAMING_VIEWER_COUNT_CACHE_STORE=array \
|
||||
MEDIAMTX_SHARED_SECRET=build-time-secret
|
||||
|
||||
COPY --from=node:22-bookworm-slim /usr/local/bin/node /usr/local/bin/node
|
||||
COPY --from=node:22-bookworm-slim /usr/local/lib/node_modules /usr/local/lib/node_modules
|
||||
RUN ln -s /usr/local/lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm \
|
||||
&& ln -s /usr/local/lib/node_modules/npm/bin/npx-cli.js /usr/local/bin/npx
|
||||
|
||||
COPY runtime/source/composer.json runtime/source/composer.lock ./
|
||||
COPY --from=composer-deps /var/www/html/vendor ./vendor
|
||||
|
||||
COPY runtime/source/package.json runtime/source/package-lock.json ./
|
||||
RUN npm ci
|
||||
|
||||
COPY runtime/source/ ./
|
||||
RUN mkdir -p \
|
||||
storage/app/private/recordings \
|
||||
storage/app/public \
|
||||
storage/framework/cache/data \
|
||||
storage/framework/sessions \
|
||||
storage/framework/views \
|
||||
storage/logs \
|
||||
bootstrap/cache \
|
||||
&& composer dump-autoload --optimize --no-dev \
|
||||
&& php artisan package:discover --ansi \
|
||||
&& php artisan route:clear \
|
||||
&& php artisan wayfinder:generate --with-form --no-interaction \
|
||||
&& npm run build
|
||||
|
||||
FROM php-base AS app
|
||||
|
||||
COPY runtime/source/composer.json runtime/source/composer.lock ./
|
||||
COPY --from=composer-deps /var/www/html/vendor ./vendor
|
||||
|
||||
COPY runtime/source/ ./
|
||||
COPY --from=assets /var/www/html/public/build ./public/build
|
||||
|
||||
COPY docker/app/apache/nyone.conf.template /etc/apache2/sites-available/nyone.conf.template
|
||||
COPY docker/app/php/production.ini /usr/local/etc/php/conf.d/production.ini
|
||||
COPY docker/app/entrypoint.sh /usr/local/bin/nyone-entrypoint
|
||||
|
||||
RUN composer dump-autoload --optimize --no-dev \
|
||||
&& php artisan package:discover --ansi \
|
||||
&& mkdir -p \
|
||||
storage/app/private/recordings \
|
||||
storage/app/public \
|
||||
storage/framework/cache/data \
|
||||
storage/framework/sessions \
|
||||
storage/framework/views \
|
||||
storage/logs \
|
||||
bootstrap/cache \
|
||||
&& if [ ! -e public/storage ]; then ln -s ../storage/app/public public/storage; fi \
|
||||
&& chown -R www-data:www-data storage bootstrap/cache \
|
||||
&& chmod +x /usr/local/bin/nyone-entrypoint
|
||||
|
||||
ENTRYPOINT ["nyone-entrypoint"]
|
||||
CMD ["app"]
|
||||
24
docker-bundle/docker/app/apache/nyone.conf.template
Normal file
24
docker-bundle/docker/app/apache/nyone.conf.template
Normal file
@@ -0,0 +1,24 @@
|
||||
ServerName ${APP_DOMAIN}
|
||||
|
||||
<VirtualHost *:80>
|
||||
ServerName ${APP_DOMAIN}
|
||||
|
||||
DocumentRoot /var/www/html/public
|
||||
DirectoryIndex index.php
|
||||
|
||||
SetEnvIf X-Forwarded-Proto "^https$" HTTPS=on
|
||||
SetEnvIf X-Forwarded-Ssl "^on$" HTTPS=on
|
||||
|
||||
<Directory /var/www/html/public>
|
||||
Options -Indexes +FollowSymLinks
|
||||
AllowOverride All
|
||||
Require all granted
|
||||
</Directory>
|
||||
|
||||
<FilesMatch "\.php$">
|
||||
SetHandler application/x-httpd-php
|
||||
</FilesMatch>
|
||||
|
||||
ErrorLog /proc/self/fd/2
|
||||
CustomLog /proc/self/fd/1 combined
|
||||
</VirtualHost>
|
||||
@@ -15,7 +15,7 @@ require_env() {
|
||||
local value="${!key:-}"
|
||||
|
||||
if [[ -z "$value" || "$value" == "change-me"* || "$value" == "base64:change-me"* ]]; then
|
||||
fail "$key is required. Run dockerized/scripts/deploy.sh or set it in dockerized/.env."
|
||||
fail "$key is required. Run docker-bundle/scripts/deploy.sh or set it in docker-bundle/.env."
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -41,9 +41,9 @@ artisan() {
|
||||
}
|
||||
|
||||
render_apache_config() {
|
||||
export APP_DOMAIN HLS_DOMAIN HLS_INTERNAL_PORT APP_URL
|
||||
export APP_DOMAIN
|
||||
|
||||
envsubst '${APP_DOMAIN} ${HLS_DOMAIN} ${HLS_INTERNAL_PORT} ${APP_URL}' \
|
||||
envsubst '${APP_DOMAIN}' \
|
||||
< /etc/apache2/sites-available/nyone.conf.template \
|
||||
> /etc/apache2/sites-available/000-default.conf
|
||||
}
|
||||
@@ -61,6 +61,16 @@ prepare_laravel_paths() {
|
||||
chown -R www-data:www-data storage bootstrap/cache
|
||||
}
|
||||
|
||||
ensure_public_storage_link() {
|
||||
mkdir -p public storage/app/public
|
||||
|
||||
if [[ -L public/storage || -e public/storage ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
ln -s ../storage/app/public public/storage
|
||||
}
|
||||
|
||||
cache_laravel_bootstrap() {
|
||||
if [[ "${APP_OPTIMIZE_ON_BOOT:-true}" != "true" ]]; then
|
||||
return 0
|
||||
@@ -93,7 +103,7 @@ if [[ "${WAIT_FOR_SERVICES:-true}" == "true" ]]; then
|
||||
fi
|
||||
|
||||
prepare_laravel_paths
|
||||
artisan storage:link --force >/dev/null || true
|
||||
ensure_public_storage_link
|
||||
cache_laravel_bootstrap
|
||||
|
||||
case "$role" in
|
||||
@@ -121,7 +131,6 @@ case "$role" in
|
||||
;;
|
||||
viewer-sync)
|
||||
exec gosu www-data php artisan streaming:sync-viewer-counts \
|
||||
--isolated \
|
||||
--interval="${STREAMING_VIEWER_SYNC_INTERVAL:-2}"
|
||||
;;
|
||||
artisan)
|
||||
@@ -7,19 +7,17 @@ fail() {
|
||||
}
|
||||
|
||||
if [ -z "${MEDIAMTX_SHARED_SECRET:-}" ] || [ "${MEDIAMTX_SHARED_SECRET#change-me}" != "$MEDIAMTX_SHARED_SECRET" ]; then
|
||||
fail "MEDIAMTX_SHARED_SECRET is required. Run dockerized/scripts/deploy.sh or set it in dockerized/.env."
|
||||
fail "MEDIAMTX_SHARED_SECRET is required. Run docker-bundle/scripts/deploy.sh or set it in docker-bundle/.env."
|
||||
fi
|
||||
|
||||
: "${APP_URL:?APP_URL is required.}"
|
||||
: "${HLS_PUBLIC_URL:?HLS_PUBLIC_URL is required.}"
|
||||
: "${RTMP_INTERNAL_PORT:=1935}"
|
||||
: "${HLS_INTERNAL_PORT:=8888}"
|
||||
|
||||
mkdir -p /config /var/www/html/storage/app/private/recordings
|
||||
|
||||
export APP_URL HLS_PUBLIC_URL MEDIAMTX_SHARED_SECRET RTMP_INTERNAL_PORT HLS_INTERNAL_PORT
|
||||
export MEDIAMTX_SHARED_SECRET RTMP_INTERNAL_PORT HLS_INTERNAL_PORT
|
||||
|
||||
envsubst '${APP_URL} ${HLS_PUBLIC_URL} ${MEDIAMTX_SHARED_SECRET} ${RTMP_INTERNAL_PORT} ${HLS_INTERNAL_PORT}' \
|
||||
envsubst '${MEDIAMTX_SHARED_SECRET} ${RTMP_INTERNAL_PORT} ${HLS_INTERNAL_PORT}' \
|
||||
< /etc/mediamtx/mediamtx.yml.template \
|
||||
> /config/mediamtx.yml
|
||||
|
||||
@@ -16,9 +16,7 @@ rtmpAddress: :${RTMP_INTERNAL_PORT}
|
||||
hls: yes
|
||||
hlsAddress: :${HLS_INTERNAL_PORT}
|
||||
hlsVariant: mpegts
|
||||
hlsAllowOrigins:
|
||||
- ${APP_URL}
|
||||
- ${HLS_PUBLIC_URL}
|
||||
hlsAllowOrigins: []
|
||||
|
||||
pathDefaults:
|
||||
record: yes
|
||||
@@ -32,4 +30,3 @@ pathDefaults:
|
||||
paths:
|
||||
all_others:
|
||||
source: publisher
|
||||
|
||||
452
docker-bundle/scripts/backup.sh
Normal file
452
docker-bundle/scripts/backup.sh
Normal file
@@ -0,0 +1,452 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
ENV_FILE="$ROOT_DIR/.env"
|
||||
COMPOSE_FILE="$ROOT_DIR/docker-compose.yml"
|
||||
SOURCE_DIR="$ROOT_DIR/runtime/source"
|
||||
BACKUP_ROOT="$ROOT_DIR/backups"
|
||||
BACKUP_NAME="nyone-backup-$(date -u +%Y%m%d-%H%M%S)"
|
||||
STOP_WRITERS=false
|
||||
FORCE=false
|
||||
STAGE_DIR=""
|
||||
RESTART_WRITERS=false
|
||||
|
||||
info() {
|
||||
printf '[nyone-backup] %s\n' "$*"
|
||||
}
|
||||
|
||||
warn() {
|
||||
printf '[nyone-backup] WARNING: %s\n' "$*" >&2
|
||||
}
|
||||
|
||||
fail() {
|
||||
printf '[nyone-backup] ERROR: %s\n' "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
usage() {
|
||||
cat <<'USAGE'
|
||||
Usage:
|
||||
scripts/backup.sh [options]
|
||||
|
||||
Options:
|
||||
--output-dir PATH Directory for the final zip file. Default: docker-bundle/backups
|
||||
--name NAME Backup folder/zip base name. Default: nyone-backup-UTC_TIMESTAMP
|
||||
--stop-writers Stop app, MediaMTX, queue, scheduler, and viewer-sync while files are archived
|
||||
--force Replace an existing zip with the same name
|
||||
-h, --help Show this help
|
||||
|
||||
The script creates one restore-ready zip containing:
|
||||
- docker-bundle deployment files and .env
|
||||
- runtime/source checkout when present, excluding dependencies and Laravel runtime cache/log files
|
||||
- PostgreSQL SQL dump
|
||||
- app storage volume archive
|
||||
- Redis data volume archive
|
||||
- restore.sh and README_RESTORE.md
|
||||
USAGE
|
||||
}
|
||||
|
||||
require_command() {
|
||||
command -v "$1" >/dev/null 2>&1 || fail "$1 is required."
|
||||
}
|
||||
|
||||
env_value() {
|
||||
local key="$1"
|
||||
|
||||
if [ ! -f "$ENV_FILE" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
awk -v key="$key" '
|
||||
index($0, key "=") == 1 {
|
||||
value = substr($0, length(key) + 2)
|
||||
}
|
||||
END {
|
||||
print value
|
||||
}
|
||||
' "$ENV_FILE"
|
||||
}
|
||||
|
||||
run_compose() {
|
||||
docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" "$@"
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
local exit_code=$?
|
||||
|
||||
if [ "$RESTART_WRITERS" = true ]; then
|
||||
info "Restarting writer services."
|
||||
run_compose up -d app mediamtx queue scheduler viewer-sync >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
if [ -n "$STAGE_DIR" ] && [[ "$STAGE_DIR" == "$BACKUP_ROOT"/.*.stage ]]; then
|
||||
rm -rf "$STAGE_DIR"
|
||||
fi
|
||||
|
||||
exit "$exit_code"
|
||||
}
|
||||
|
||||
trap cleanup EXIT
|
||||
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
--output-dir)
|
||||
[ "$#" -ge 2 ] || fail "--output-dir requires a value."
|
||||
BACKUP_ROOT="$2"
|
||||
shift 2
|
||||
;;
|
||||
--name)
|
||||
[ "$#" -ge 2 ] || fail "--name requires a value."
|
||||
BACKUP_NAME="$2"
|
||||
shift 2
|
||||
;;
|
||||
--stop-writers)
|
||||
STOP_WRITERS=true
|
||||
shift
|
||||
;;
|
||||
--force)
|
||||
FORCE=true
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
fail "Unknown option: $1"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
case "$BACKUP_NAME" in
|
||||
*/*|*\\*)
|
||||
fail "--name must be a file name, not a path."
|
||||
;;
|
||||
esac
|
||||
|
||||
[ -f "$ENV_FILE" ] || fail "$ENV_FILE was not found. Create it before running a backup."
|
||||
[ -f "$COMPOSE_FILE" ] || fail "$COMPOSE_FILE was not found."
|
||||
|
||||
require_command docker
|
||||
require_command tar
|
||||
require_command gzip
|
||||
require_command zip
|
||||
require_command sha256sum
|
||||
|
||||
mkdir -p "$BACKUP_ROOT"
|
||||
BACKUP_ROOT="$(cd "$BACKUP_ROOT" && pwd)"
|
||||
STAGE_DIR="$BACKUP_ROOT/.${BACKUP_NAME}.stage"
|
||||
ARCHIVE_DIR="$STAGE_DIR/$BACKUP_NAME"
|
||||
ZIP_PATH="$BACKUP_ROOT/$BACKUP_NAME.zip"
|
||||
|
||||
if [ -e "$ZIP_PATH" ] && [ "$FORCE" != true ]; then
|
||||
fail "$ZIP_PATH already exists. Use --force or choose another --name."
|
||||
fi
|
||||
|
||||
rm -rf "$STAGE_DIR"
|
||||
mkdir -p "$ARCHIVE_DIR"/{database,deployment,metadata,volumes}
|
||||
|
||||
info "Validating Compose configuration."
|
||||
run_compose config > "$ARCHIVE_DIR/metadata/compose.resolved.yml"
|
||||
|
||||
if [ "$STOP_WRITERS" = true ]; then
|
||||
info "Stopping writer services for a consistent file backup."
|
||||
run_compose stop app mediamtx queue scheduler viewer-sync >/dev/null || true
|
||||
RESTART_WRITERS=true
|
||||
fi
|
||||
|
||||
info "Writing PostgreSQL dump."
|
||||
run_compose exec -T db sh -c 'PGPASSWORD="$POSTGRES_PASSWORD" pg_dump -h 127.0.0.1 -U "$POSTGRES_USER" -d "$POSTGRES_DB" --clean --if-exists --no-owner --no-privileges' \
|
||||
| gzip -9 > "$ARCHIVE_DIR/database/postgres.sql.gz"
|
||||
|
||||
info "Archiving app storage volume."
|
||||
run_compose run --rm --no-deps -T --user root --entrypoint tar \
|
||||
-v "$ARCHIVE_DIR/volumes:/backup" \
|
||||
app -czf /backup/app-storage.tar.gz -C /var/www/html/storage .
|
||||
|
||||
info "Archiving Redis data volume."
|
||||
if ! run_compose exec -T redis redis-cli SAVE >/dev/null; then
|
||||
warn "Could not force a Redis SAVE before archiving; continuing with the current volume files."
|
||||
fi
|
||||
|
||||
run_compose run --rm --no-deps -T --user root --entrypoint tar \
|
||||
-v "$ARCHIVE_DIR/volumes:/backup" \
|
||||
redis -czf /backup/redis-data.tar.gz -C /data .
|
||||
|
||||
info "Archiving docker-bundle deployment files."
|
||||
tar -czf "$ARCHIVE_DIR/deployment/docker-bundle-files.tar.gz" \
|
||||
-C "$(dirname "$ROOT_DIR")" \
|
||||
--exclude='docker-bundle/backups' \
|
||||
--exclude='docker-bundle/runtime' \
|
||||
"$(basename "$ROOT_DIR")"
|
||||
|
||||
if [ -d "$SOURCE_DIR" ]; then
|
||||
info "Archiving runtime/source checkout."
|
||||
tar -czf "$ARCHIVE_DIR/deployment/runtime-source.tar.gz" \
|
||||
-C "$(dirname "$SOURCE_DIR")" \
|
||||
--exclude='source/.env' \
|
||||
--exclude='source/node_modules' \
|
||||
--exclude='source/vendor' \
|
||||
--exclude='source/public/build' \
|
||||
--exclude='source/storage/app/private/recordings' \
|
||||
--exclude='source/storage/framework/cache' \
|
||||
--exclude='source/storage/framework/sessions' \
|
||||
--exclude='source/storage/framework/testing' \
|
||||
--exclude='source/storage/framework/views' \
|
||||
--exclude='source/storage/logs' \
|
||||
source
|
||||
else
|
||||
warn "$SOURCE_DIR was not found; the archive will rely on GIT_REPOSITORY_URL for source restoration."
|
||||
fi
|
||||
|
||||
info "Writing metadata."
|
||||
{
|
||||
printf 'created_at_utc=%s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
printf 'compose_project_name=%s\n' "$(env_value COMPOSE_PROJECT_NAME)"
|
||||
printf 'app_domain=%s\n' "$(env_value APP_DOMAIN)"
|
||||
printf 'hls_domain=%s\n' "$(env_value HLS_DOMAIN)"
|
||||
printf 'git_repository_url=%s\n' "$(env_value GIT_REPOSITORY_URL)"
|
||||
printf 'git_ref=%s\n' "$(env_value GIT_REF)"
|
||||
printf 'backup_name=%s\n' "$BACKUP_NAME"
|
||||
printf 'stop_writers=%s\n' "$STOP_WRITERS"
|
||||
} > "$ARCHIVE_DIR/metadata/manifest.env"
|
||||
|
||||
docker version > "$ARCHIVE_DIR/metadata/docker-version.txt" 2>&1 || true
|
||||
docker compose version > "$ARCHIVE_DIR/metadata/docker-compose-version.txt" 2>&1 || true
|
||||
run_compose ps > "$ARCHIVE_DIR/metadata/container-status.txt" 2>&1 || true
|
||||
|
||||
if [ -d "$SOURCE_DIR/.git" ]; then
|
||||
{
|
||||
git -C "$SOURCE_DIR" remote -v
|
||||
git -C "$SOURCE_DIR" rev-parse HEAD
|
||||
git -C "$SOURCE_DIR" status --short
|
||||
} > "$ARCHIVE_DIR/metadata/source-git.txt" 2>&1 || true
|
||||
fi
|
||||
|
||||
cat > "$ARCHIVE_DIR/README_RESTORE.md" <<'RESTORE_README'
|
||||
# Nyone docker-bundle Backup Restore
|
||||
|
||||
This archive contains everything managed by the `docker-bundle/` deployment bundle:
|
||||
|
||||
- deployment files and `.env`
|
||||
- `runtime/source` checkout when it existed at backup time
|
||||
- PostgreSQL dump
|
||||
- Laravel app storage volume
|
||||
- Redis data volume
|
||||
|
||||
Host-level files outside `docker-bundle/`, such as Nginx site files and Let's Encrypt certificates, are not included.
|
||||
|
||||
## Restore on a fresh server
|
||||
|
||||
Install Docker and the Compose plugin first. Then extract this zip and run:
|
||||
|
||||
```bash
|
||||
chmod +x restore.sh
|
||||
./restore.sh /opt/nyone/docker-bundle
|
||||
```
|
||||
|
||||
If the target already exists and you intentionally want to replace it:
|
||||
|
||||
```bash
|
||||
./restore.sh --replace /opt/nyone/docker-bundle
|
||||
```
|
||||
|
||||
The restore helper copies the deployment bundle, restores `runtime/source` when present, clones it from `.env` when missing, rebuilds the images, restores PostgreSQL, restores the app storage and Redis volumes, and starts the app services.
|
||||
|
||||
After restore, re-create host-level Nginx/TLS configuration if this is a new VPS.
|
||||
RESTORE_README
|
||||
|
||||
cat > "$ARCHIVE_DIR/restore.sh" <<'RESTORE_SCRIPT'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
BACKUP_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
TARGET_DIR="/opt/nyone/docker-bundle"
|
||||
REPLACE=false
|
||||
TMP_DIR=""
|
||||
|
||||
info() {
|
||||
printf '[nyone-restore] %s\n' "$*"
|
||||
}
|
||||
|
||||
fail() {
|
||||
printf '[nyone-restore] ERROR: %s\n' "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
usage() {
|
||||
cat <<'USAGE'
|
||||
Usage:
|
||||
./restore.sh [--replace] [target-dir]
|
||||
|
||||
Example:
|
||||
./restore.sh /opt/nyone/docker-bundle
|
||||
./restore.sh --replace /opt/nyone/docker-bundle
|
||||
USAGE
|
||||
}
|
||||
|
||||
require_command() {
|
||||
command -v "$1" >/dev/null 2>&1 || fail "$1 is required."
|
||||
}
|
||||
|
||||
env_value() {
|
||||
local key="$1"
|
||||
local env_file="$TARGET_DIR/.env"
|
||||
|
||||
if [ ! -f "$env_file" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
awk -v key="$key" '
|
||||
index($0, key "=") == 1 {
|
||||
value = substr($0, length(key) + 2)
|
||||
}
|
||||
END {
|
||||
print value
|
||||
}
|
||||
' "$env_file"
|
||||
}
|
||||
|
||||
ensure_source_checkout() {
|
||||
local repository_url
|
||||
local git_ref
|
||||
|
||||
if [ -d "$TARGET_DIR/runtime/source" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
repository_url="$(env_value GIT_REPOSITORY_URL)"
|
||||
git_ref="$(env_value GIT_REF)"
|
||||
git_ref="${git_ref:-main}"
|
||||
|
||||
[ -n "$repository_url" ] || fail "runtime/source is missing and GIT_REPOSITORY_URL is not set in .env."
|
||||
|
||||
require_command git
|
||||
|
||||
info "Cloning runtime/source from GIT_REPOSITORY_URL."
|
||||
mkdir -p "$TARGET_DIR/runtime"
|
||||
git clone "$repository_url" "$TARGET_DIR/runtime/source"
|
||||
git -C "$TARGET_DIR/runtime/source" checkout "$git_ref"
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
if [ -n "$TMP_DIR" ] && [ -d "$TMP_DIR" ]; then
|
||||
rm -rf "$TMP_DIR"
|
||||
fi
|
||||
}
|
||||
|
||||
trap cleanup EXIT
|
||||
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
--replace)
|
||||
REPLACE=true
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
TARGET_DIR="$1"
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
[ "$TARGET_DIR" != "/" ] || fail "Refusing to restore into /."
|
||||
[ -f "$BACKUP_DIR/deployment/docker-bundle-files.tar.gz" ] || fail "deployment/docker-bundle-files.tar.gz is missing."
|
||||
[ -f "$BACKUP_DIR/database/postgres.sql.gz" ] || fail "database/postgres.sql.gz is missing."
|
||||
[ -f "$BACKUP_DIR/volumes/app-storage.tar.gz" ] || fail "volumes/app-storage.tar.gz is missing."
|
||||
|
||||
require_command docker
|
||||
require_command tar
|
||||
require_command gzip
|
||||
|
||||
TARGET_PARENT="$(dirname "$TARGET_DIR")"
|
||||
mkdir -p "$TARGET_PARENT"
|
||||
|
||||
if [ -e "$TARGET_DIR" ]; then
|
||||
if [ "$REPLACE" != true ]; then
|
||||
fail "$TARGET_DIR already exists. Use --replace if you want to replace it."
|
||||
fi
|
||||
|
||||
if [ -f "$TARGET_DIR/docker-compose.yml" ] && [ -f "$TARGET_DIR/.env" ]; then
|
||||
info "Stopping existing Compose stack."
|
||||
docker compose --env-file "$TARGET_DIR/.env" -f "$TARGET_DIR/docker-compose.yml" down || true
|
||||
fi
|
||||
|
||||
rm -rf "$TARGET_DIR"
|
||||
fi
|
||||
|
||||
TMP_DIR="$(mktemp -d)"
|
||||
info "Restoring docker-bundle files."
|
||||
tar -xzf "$BACKUP_DIR/deployment/docker-bundle-files.tar.gz" -C "$TMP_DIR"
|
||||
[ -d "$TMP_DIR/docker-bundle" ] || fail "Backup did not contain a docker-bundle directory."
|
||||
mv "$TMP_DIR/docker-bundle" "$TARGET_DIR"
|
||||
|
||||
if [ -f "$BACKUP_DIR/deployment/runtime-source.tar.gz" ]; then
|
||||
info "Restoring runtime/source."
|
||||
mkdir -p "$TARGET_DIR/runtime"
|
||||
tar -xzf "$BACKUP_DIR/deployment/runtime-source.tar.gz" -C "$TARGET_DIR/runtime"
|
||||
fi
|
||||
|
||||
chmod +x "$TARGET_DIR"/scripts/*.sh
|
||||
|
||||
cd "$TARGET_DIR"
|
||||
ensure_source_checkout
|
||||
|
||||
run_compose() {
|
||||
docker compose --env-file "$TARGET_DIR/.env" -f "$TARGET_DIR/docker-compose.yml" "$@"
|
||||
}
|
||||
|
||||
info "Validating Compose configuration."
|
||||
run_compose config >/dev/null
|
||||
|
||||
info "Building Docker images."
|
||||
run_compose build
|
||||
|
||||
info "Starting PostgreSQL."
|
||||
run_compose up -d db
|
||||
run_compose exec -T db sh -c 'until pg_isready -h 127.0.0.1 -U "$POSTGRES_USER" -d "$POSTGRES_DB"; do sleep 2; done'
|
||||
|
||||
info "Restoring PostgreSQL dump."
|
||||
gzip -dc "$BACKUP_DIR/database/postgres.sql.gz" \
|
||||
| run_compose exec -T db sh -c 'PGPASSWORD="$POSTGRES_PASSWORD" psql -h 127.0.0.1 -U "$POSTGRES_USER" -d "$POSTGRES_DB" -v ON_ERROR_STOP=1'
|
||||
|
||||
info "Restoring app storage volume."
|
||||
run_compose run --rm --no-deps -T --user root --entrypoint sh \
|
||||
-v "$BACKUP_DIR/volumes:/backup:ro" \
|
||||
app -c 'find /var/www/html/storage -mindepth 1 -maxdepth 1 -exec rm -rf {} + && tar -xzf /backup/app-storage.tar.gz -C /var/www/html/storage'
|
||||
|
||||
if [ -f "$BACKUP_DIR/volumes/redis-data.tar.gz" ]; then
|
||||
info "Restoring Redis data volume."
|
||||
run_compose up -d redis
|
||||
run_compose stop redis >/dev/null || true
|
||||
run_compose run --rm --no-deps -T --user root --entrypoint sh \
|
||||
-v "$BACKUP_DIR/volumes:/backup:ro" \
|
||||
redis -c 'find /data -mindepth 1 -maxdepth 1 -exec rm -rf {} + && tar -xzf /backup/redis-data.tar.gz -C /data'
|
||||
fi
|
||||
|
||||
info "Starting application services."
|
||||
run_compose up -d --remove-orphans app mediamtx queue scheduler viewer-sync redis
|
||||
run_compose ps
|
||||
RESTORE_SCRIPT
|
||||
|
||||
chmod +x "$ARCHIVE_DIR/restore.sh"
|
||||
|
||||
info "Writing checksums."
|
||||
(
|
||||
cd "$ARCHIVE_DIR"
|
||||
find deployment database volumes metadata -type f -print0 | sort -z | xargs -0 sha256sum > checksums.sha256
|
||||
)
|
||||
|
||||
info "Creating zip archive."
|
||||
rm -f "$ZIP_PATH"
|
||||
(
|
||||
cd "$STAGE_DIR"
|
||||
zip -rq "$ZIP_PATH" "$BACKUP_NAME"
|
||||
)
|
||||
|
||||
info "Backup ready: $ZIP_PATH"
|
||||
@@ -158,7 +158,7 @@ require_config_value APP_DOMAIN "nyone.example.com"
|
||||
require_config_value HLS_DOMAIN "nyone-hls.example.com"
|
||||
|
||||
if [ "$(env_value APP_DOMAIN)" = "$(env_value HLS_DOMAIN)" ]; then
|
||||
fail "APP_DOMAIN and HLS_DOMAIN must be different because this deployment uses separate Apache virtual hosts."
|
||||
fail "APP_DOMAIN and HLS_DOMAIN must be different because this deployment uses separate app and HLS hostnames."
|
||||
fi
|
||||
|
||||
GIT_REF="${GIT_REF:-main}"
|
||||
@@ -1,70 +0,0 @@
|
||||
---
|
||||
name: dockerized-deployment
|
||||
description: Work on the Nyone Dockerized deployment bundle under dockerized/. Use when modifying Docker Compose, Apache, MediaMTX, PostgreSQL, Redis, Laravel queue/scheduler/viewer-sync services, deploy.sh, deployment README files, or runtime environment defaults for this self-contained Docker deployment.
|
||||
---
|
||||
|
||||
# Nyone Dockerized Deployment
|
||||
|
||||
## Core Rules
|
||||
|
||||
- Keep deployment changes inside `dockerized/` unless the user explicitly requests application code changes.
|
||||
- Treat `dockerized/.env` and `dockerized/runtime/` as server-local generated state; do not commit or depend on them.
|
||||
- Do not make this bundle depend on the cloned app repository's `.env` or `deploy/mediamtx.yml`.
|
||||
- Keep MediaMTX config Docker-owned at `docker/mediamtx/mediamtx.yml.template`.
|
||||
- Keep `APP_DOMAIN` and `HLS_DOMAIN` separate because Apache uses separate virtual hosts.
|
||||
- Do not run `scripts/deploy.sh`, `docker compose up`, or image builds unless the user confirms this is a deployment server.
|
||||
|
||||
## Architecture
|
||||
|
||||
- `scripts/deploy.sh` is the entrypoint for server deployment.
|
||||
- `docker-compose.yml` defines `app`, `db`, `redis`, `mediamtx`, `queue`, `scheduler`, `viewer-sync`, and one-shot `migrate`.
|
||||
- `docker/app/Dockerfile` builds the Laravel Apache image from `runtime/source`.
|
||||
- `docker/app/entrypoint.sh` selects behavior by `CONTAINER_ROLE`.
|
||||
- `docker/app/apache/nyone.conf.template` serves Laravel on `APP_DOMAIN` and proxies HLS for `HLS_DOMAIN`.
|
||||
- `docker/mediamtx/entrypoint.sh` renders the MediaMTX template at container start.
|
||||
|
||||
## Port Defaults
|
||||
|
||||
- Apache published HTTP: `80`.
|
||||
- MediaMTX RTMP: `1935`.
|
||||
- MediaMTX HLS internal: `8888`.
|
||||
- MediaMTX API internal: `9997`.
|
||||
- PostgreSQL internal: `5432`.
|
||||
- Redis internal: `6379`.
|
||||
|
||||
Only publish HTTP and RTMP by default. Keep MediaMTX HLS behind Apache and keep the API private on the Docker network.
|
||||
|
||||
## Editing Guidance
|
||||
|
||||
- When adding an environment option, update `.env.example`, `docker-compose.yml`, and any entrypoint/template that consumes it.
|
||||
- When changing MediaMTX ports, update Compose environment, published ports, entrypoint defaults, and the template together.
|
||||
- When changing Laravel runtime behavior, update the shared `x-app-environment` block unless the value belongs to one role only.
|
||||
- When changing queue behavior, prefer env-driven options on the `queue` service and `docker/app/entrypoint.sh`.
|
||||
- Keep deploy script behavior idempotent: repeat runs should update the Git checkout, rebuild, migrate, and restart services without deleting volumes.
|
||||
- Keep generated secrets in `.env`; do not bake secrets into images or templates.
|
||||
|
||||
## Nginx TLS Proxy Guidance
|
||||
|
||||
- Document Nginx as a host-level TLS reverse proxy in `README.md`; do not add Nginx as another Compose service unless the user asks.
|
||||
- Keep Docker Apache as the upstream for both app and HLS hostnames. Nginx should proxy both domains to the published Apache port and preserve the `Host` header so Apache can select the correct virtual host.
|
||||
- Recommend `APP_HTTP_PORT=127.0.0.1:8080` when Nginx owns host ports `80` and `443`.
|
||||
- Include `X-Forwarded-Proto https`, `X-Forwarded-Port 443`, `X-Forwarded-Host`, `X-Real-IP`, and `X-Forwarded-For` in proxy examples.
|
||||
- Keep HLS proxy examples buffering off and cache headers set to no-store.
|
||||
- Keep RTMP outside the HTTP proxy guidance unless explicitly using Nginx stream proxying; the default is direct TCP `1935` to MediaMTX through Docker.
|
||||
|
||||
## Static Verification
|
||||
|
||||
Use static checks when not on a deployment server:
|
||||
|
||||
```bash
|
||||
git diff --check
|
||||
rg "1993[5]|1998[8]|80[8]0|25[2]5|nyone[.]net|nyone-hls[.]net|change-this-secre[t]" dockerized -n
|
||||
```
|
||||
|
||||
If Docker is available and the user allows non-deploy validation, validate only the Compose config:
|
||||
|
||||
```bash
|
||||
docker compose --env-file dockerized/.env.example -f dockerized/docker-compose.yml config
|
||||
```
|
||||
|
||||
Do not start containers during local verification unless the user explicitly asks for runtime testing.
|
||||
@@ -1,75 +0,0 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
FROM node:22-bookworm-slim AS assets
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
COPY runtime/source/package.json runtime/source/package-lock.json ./
|
||||
RUN npm ci
|
||||
|
||||
COPY runtime/source/ ./
|
||||
RUN npm run build
|
||||
|
||||
FROM php:8.4-apache-bookworm AS app
|
||||
|
||||
ENV APACHE_DOCUMENT_ROOT=/var/www/html/public \
|
||||
COMPOSER_ALLOW_SUPERUSER=1
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
curl \
|
||||
gettext-base \
|
||||
git \
|
||||
gosu \
|
||||
libicu-dev \
|
||||
libcurl4-openssl-dev \
|
||||
libonig-dev \
|
||||
libpq-dev \
|
||||
libzip-dev \
|
||||
netcat-openbsd \
|
||||
unzip \
|
||||
&& docker-php-ext-install -j"$(nproc)" \
|
||||
bcmath \
|
||||
curl \
|
||||
intl \
|
||||
mbstring \
|
||||
opcache \
|
||||
pcntl \
|
||||
pdo_pgsql \
|
||||
pgsql \
|
||||
zip \
|
||||
&& pecl install redis \
|
||||
&& docker-php-ext-enable redis \
|
||||
&& a2enmod headers proxy proxy_http rewrite setenvif \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
|
||||
|
||||
WORKDIR /var/www/html
|
||||
|
||||
COPY runtime/source/composer.json runtime/source/composer.lock ./
|
||||
RUN composer install --no-dev --no-interaction --prefer-dist --optimize-autoloader --no-scripts
|
||||
|
||||
COPY runtime/source/ ./
|
||||
COPY --from=assets /build/public/build ./public/build
|
||||
|
||||
COPY docker/app/apache/nyone.conf.template /etc/apache2/sites-available/nyone.conf.template
|
||||
COPY docker/app/php/production.ini /usr/local/etc/php/conf.d/production.ini
|
||||
COPY docker/app/entrypoint.sh /usr/local/bin/nyone-entrypoint
|
||||
|
||||
RUN composer dump-autoload --optimize --no-dev \
|
||||
&& php artisan package:discover --ansi \
|
||||
&& mkdir -p \
|
||||
storage/app/private/recordings \
|
||||
storage/app/public \
|
||||
storage/framework/cache/data \
|
||||
storage/framework/sessions \
|
||||
storage/framework/views \
|
||||
storage/logs \
|
||||
bootstrap/cache \
|
||||
&& chown -R www-data:www-data storage bootstrap/cache \
|
||||
&& chmod +x /usr/local/bin/nyone-entrypoint
|
||||
|
||||
ENTRYPOINT ["nyone-entrypoint"]
|
||||
CMD ["app"]
|
||||
@@ -1,43 +0,0 @@
|
||||
ServerName ${APP_DOMAIN}
|
||||
|
||||
<VirtualHost *:80>
|
||||
ServerName ${APP_DOMAIN}
|
||||
|
||||
DocumentRoot /var/www/html/public
|
||||
DirectoryIndex index.php
|
||||
|
||||
SetEnvIf X-Forwarded-Proto "^https$" HTTPS=on
|
||||
SetEnvIf X-Forwarded-Ssl "^on$" HTTPS=on
|
||||
|
||||
<Directory /var/www/html/public>
|
||||
Options -Indexes +FollowSymLinks
|
||||
AllowOverride All
|
||||
Require all granted
|
||||
</Directory>
|
||||
|
||||
<FilesMatch "\.php$">
|
||||
SetHandler application/x-httpd-php
|
||||
</FilesMatch>
|
||||
|
||||
ErrorLog /proc/self/fd/2
|
||||
CustomLog /proc/self/fd/1 combined
|
||||
</VirtualHost>
|
||||
|
||||
<VirtualHost *:80>
|
||||
ServerName ${HLS_DOMAIN}
|
||||
|
||||
ProxyRequests Off
|
||||
ProxyPreserveHost On
|
||||
|
||||
ProxyPass "/" "http://mediamtx:${HLS_INTERNAL_PORT}/" retry=0
|
||||
ProxyPassReverse "/" "http://mediamtx:${HLS_INTERNAL_PORT}/"
|
||||
|
||||
Header always set Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate"
|
||||
Header always set Access-Control-Allow-Origin "${APP_URL}"
|
||||
Header always set Access-Control-Allow-Methods "GET, HEAD, OPTIONS"
|
||||
Header always set Access-Control-Allow-Headers "Range, Origin, Accept, Content-Type"
|
||||
|
||||
ErrorLog /proc/self/fd/2
|
||||
CustomLog /proc/self/fd/1 combined
|
||||
</VirtualHost>
|
||||
|
||||
@@ -157,16 +157,16 @@ admin@example.com / password
|
||||
Current `.env` values relevant to streaming:
|
||||
|
||||
```env
|
||||
APP_URL=https://nyone.net
|
||||
STREAMING_RTMP_INGEST_URL=rtmp://nyone.net:19935
|
||||
APP_URL=https://nyone.app
|
||||
STREAMING_RTMP_INGEST_URL=rtmp://nyone.app:19935
|
||||
STREAMING_HLS_PUBLIC_URL=https://nyone-hls.net
|
||||
MEDIAMTX_SHARED_SECRET=change-this-secret
|
||||
```
|
||||
|
||||
MediaMTX current files:
|
||||
|
||||
- `deploy/mediamtx.yml`: currently points auth/hooks to `https://nyone.net`.
|
||||
- `deploy/mediamtx.home.yml`: points auth/hooks to `https://nyone.net`.
|
||||
- `deploy/mediamtx.yml`: currently points auth/hooks to `https://nyone.app`.
|
||||
- `deploy/mediamtx.home.yml`: points auth/hooks to `https://nyone.app`.
|
||||
|
||||
MediaMTX ports currently configured:
|
||||
|
||||
@@ -176,7 +176,7 @@ MediaMTX ports currently configured:
|
||||
OBS should use:
|
||||
|
||||
```text
|
||||
Server: rtmp://nyone.net:19935
|
||||
Server: rtmp://nyone.app:19935
|
||||
Stream Key: <channel-slug>?token=<generated-stream-key>
|
||||
```
|
||||
|
||||
|
||||
Reference in New Issue
Block a user