implement the Dockerized deployment bundle

This commit is contained in:
2026-05-19 15:00:21 +03:30
parent f535530537
commit 4536ab77ee
12 changed files with 792 additions and 0 deletions

13
dockerized/.dockerignore Normal file
View File

@@ -0,0 +1,13 @@
.env
runtime/source/.env
runtime/source/.git
runtime/source/node_modules
runtime/source/vendor
runtime/source/public/build
runtime/source/storage/app/private/recordings
runtime/source/storage/framework/cache
runtime/source/storage/framework/sessions
runtime/source/storage/framework/testing
runtime/source/storage/framework/views
runtime/source/storage/logs

48
dockerized/.env.example Normal file
View File

@@ -0,0 +1,48 @@
COMPOSE_PROJECT_NAME=nyone
# Git source cloned by scripts/deploy.sh into runtime/source before Docker builds.
GIT_REPOSITORY_URL=https://github.com/your-org/nyone.git
GIT_REF=main
# Public hostnames. TLS should terminate at your external proxy/CDN/load balancer.
PUBLIC_SCHEME=https
APP_DOMAIN=nyone.example.com
HLS_DOMAIN=nyone-hls.example.com
APP_HTTP_PORT=8080
RTMP_PUBLIC_PORT=19935
APP_NAME=Nyone
APP_ENV=production
APP_DEBUG=false
APP_KEY=
APP_PREVIOUS_KEYS=
APP_LOCALE=en
APP_FALLBACK_LOCALE=en
APP_FAKER_LOCALE=en_US
LOG_LEVEL=info
MAIL_MAILER=log
MAIL_FROM_ADDRESS=hello@example.com
MAIL_FROM_NAME=Nyone
POSTGRES_DB=nyone
POSTGRES_USER=nyone
POSTGRES_PASSWORD=
MEDIAMTX_SHARED_SECRET=
STREAMING_VIEWER_SYNC_INTERVAL=2
STREAMING_VIEWER_COUNT_TTL=10
STREAMING_VIEWER_IDENTITY_COOKIE=nyone_visitor_id
STREAMING_VIEWER_IDENTITY_COOKIE_LIFETIME_DAYS=365
STREAMING_VIEWER_IDENTITY_TTL=86400
STREAMING_VIEWER_STATS_POLL_INTERVAL=30000
STREAMING_VOD_RETENTION_DAYS=30
# Optional deployment behavior.
SEED_DATABASE=false
APP_OPTIMIZE_ON_BOOT=true
QUEUE_NAMES=default
QUEUE_SLEEP=3
QUEUE_TRIES=3
QUEUE_MAX_TIME=3600
QUEUE_TIMEOUT=90

3
dockerized/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
.env
runtime/

View File

@@ -0,0 +1,188 @@
name: ${COMPOSE_PROJECT_NAME:-nyone-service}
x-app-environment: &app-environment
APP_NAME: ${APP_NAME:-Nyone}
APP_ENV: ${APP_ENV:-production}
APP_DEBUG: ${APP_DEBUG:-false}
APP_KEY: ${APP_KEY:-}
APP_PREVIOUS_KEYS: ${APP_PREVIOUS_KEYS:-}
APP_URL: ${PUBLIC_SCHEME:-https}://${APP_DOMAIN:-nyone.example.com}
APP_LOCALE: ${APP_LOCALE:-en}
APP_FALLBACK_LOCALE: ${APP_FALLBACK_LOCALE:-en}
APP_FAKER_LOCALE: ${APP_FAKER_LOCALE:-en_US}
APP_MAINTENANCE_DRIVER: file
BCRYPT_ROUNDS: 12
LOG_CHANNEL: stack
LOG_STACK: stderr
LOG_DEPRECATIONS_CHANNEL: "null"
LOG_LEVEL: ${LOG_LEVEL:-info}
DB_CONNECTION: pgsql
DB_HOST: db
DB_PORT: 5432
DB_DATABASE: ${POSTGRES_DB:-nyone}
DB_USERNAME: ${POSTGRES_USER:-nyone}
DB_PASSWORD: ${POSTGRES_PASSWORD:-}
SESSION_DRIVER: database
SESSION_LIFETIME: 120
SESSION_ENCRYPT: "false"
SESSION_PATH: /
SESSION_DOMAIN: ${SESSION_DOMAIN:-}
SESSION_SECURE_COOKIE: ${SESSION_SECURE_COOKIE:-true}
BROADCAST_CONNECTION: log
FILESYSTEM_DISK: local
QUEUE_CONNECTION: database
CACHE_STORE: redis
REDIS_CLIENT: phpredis
REDIS_HOST: redis
REDIS_PORT: 6379
MAIL_MAILER: ${MAIL_MAILER:-log}
MAIL_SCHEME: ${MAIL_SCHEME:-}
MAIL_HOST: ${MAIL_HOST:-127.0.0.1}
MAIL_PORT: ${MAIL_PORT:-2525}
MAIL_USERNAME: ${MAIL_USERNAME:-}
MAIL_PASSWORD: ${MAIL_PASSWORD:-}
MAIL_FROM_ADDRESS: ${MAIL_FROM_ADDRESS:-hello@example.com}
MAIL_FROM_NAME: ${MAIL_FROM_NAME:-Nyone}
VITE_APP_NAME: ${APP_NAME:-Nyone}
STREAMING_RTMP_INGEST_URL: rtmp://${APP_DOMAIN:-nyone.example.com}:${RTMP_PUBLIC_PORT:-19935}
STREAMING_HLS_PUBLIC_URL: ${PUBLIC_SCHEME:-https}://${HLS_DOMAIN:-nyone-hls.example.com}
STREAMING_MEDIAMTX_API_URL: http://mediamtx:9997
STREAMING_VIEWER_COUNT_CACHE_STORE: redis
STREAMING_VIEWER_COUNT_TTL: ${STREAMING_VIEWER_COUNT_TTL:-10}
STREAMING_VIEWER_SYNC_INTERVAL: ${STREAMING_VIEWER_SYNC_INTERVAL:-2}
STREAMING_VIEWER_IDENTITY_COOKIE: ${STREAMING_VIEWER_IDENTITY_COOKIE:-nyone_visitor_id}
STREAMING_VIEWER_IDENTITY_COOKIE_LIFETIME_DAYS: ${STREAMING_VIEWER_IDENTITY_COOKIE_LIFETIME_DAYS:-365}
STREAMING_VIEWER_IDENTITY_TTL: ${STREAMING_VIEWER_IDENTITY_TTL:-86400}
STREAMING_VIEWER_STATS_POLL_INTERVAL: ${STREAMING_VIEWER_STATS_POLL_INTERVAL:-30000}
STREAMING_VOD_RETENTION_DAYS: ${STREAMING_VOD_RETENTION_DAYS:-30}
STREAMING_RECORDINGS_PATH: /var/www/html/storage/app/private/recordings
MEDIAMTX_SHARED_SECRET: ${MEDIAMTX_SHARED_SECRET:-}
APP_DOMAIN: ${APP_DOMAIN:-nyone.example.com}
HLS_DOMAIN: ${HLS_DOMAIN:-nyone-hls.example.com}
HLS_INTERNAL_PORT: 19988
APP_OPTIMIZE_ON_BOOT: ${APP_OPTIMIZE_ON_BOOT:-true}
SEED_DATABASE: ${SEED_DATABASE:-false}
x-app-service: &app-service
image: ${APP_IMAGE:-nyone-app:latest}
build:
context: .
dockerfile: docker/app/Dockerfile
restart: unless-stopped
environment:
<<: *app-environment
volumes:
- app-storage:/var/www/html/storage
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
services:
app:
<<: *app-service
environment:
<<: *app-environment
CONTAINER_ROLE: app
ports:
- "${APP_HTTP_PORT:-8080}:80"
healthcheck:
test: ["CMD-SHELL", "curl -fsS http://127.0.0.1/up >/dev/null || exit 1"]
interval: 30s
timeout: 5s
retries: 5
start_period: 30s
migrate:
<<: *app-service
restart: "no"
environment:
<<: *app-environment
CONTAINER_ROLE: migrate
profiles:
- tools
queue:
<<: *app-service
environment:
<<: *app-environment
CONTAINER_ROLE: queue
QUEUE_NAMES: ${QUEUE_NAMES:-default}
QUEUE_SLEEP: ${QUEUE_SLEEP:-3}
QUEUE_TRIES: ${QUEUE_TRIES:-3}
QUEUE_MAX_TIME: ${QUEUE_MAX_TIME:-3600}
QUEUE_TIMEOUT: ${QUEUE_TIMEOUT:-90}
scheduler:
<<: *app-service
environment:
<<: *app-environment
CONTAINER_ROLE: scheduler
viewer-sync:
<<: *app-service
environment:
<<: *app-environment
CONTAINER_ROLE: viewer-sync
MEDIAMTX_HOST: mediamtx
MEDIAMTX_API_PORT: 9997
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
mediamtx:
condition: service_started
mediamtx:
image: ${MEDIAMTX_IMAGE:-nyone-mediamtx:latest}
build:
context: .
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: 19935
HLS_INTERNAL_PORT: 19988
ports:
- "${RTMP_PUBLIC_PORT:-19935}:19935"
volumes:
- app-storage:/var/www/html/storage
depends_on:
app:
condition: service_started
db:
image: postgres:17-alpine
restart: unless-stopped
environment:
POSTGRES_DB: ${POSTGRES_DB:-nyone}
POSTGRES_USER: ${POSTGRES_USER:-nyone}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-}
volumes:
- postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U \"$${POSTGRES_USER}\" -d \"$${POSTGRES_DB}\""]
interval: 10s
timeout: 5s
retries: 10
redis:
image: redis:7-alpine
restart: unless-stopped
command: ["redis-server", "--appendonly", "yes"]
volumes:
- redis-data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 10
volumes:
app-storage:
postgres-data:
redis-data:

View File

@@ -0,0 +1,75 @@
# 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"]

View File

@@ -0,0 +1,43 @@
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>

View File

@@ -0,0 +1,134 @@
#!/usr/bin/env bash
set -euo pipefail
cd /var/www/html
role="${CONTAINER_ROLE:-${1:-app}}"
fail() {
echo "nyone-entrypoint: $*" >&2
exit 1
}
require_env() {
local key="$1"
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."
fi
}
wait_for() {
local host="$1"
local port="$2"
local label="$3"
local attempts="${4:-60}"
for ((i = 1; i <= attempts; i++)); do
if nc -z "$host" "$port" >/dev/null 2>&1; then
return 0
fi
sleep 2
done
fail "Timed out waiting for $label at $host:$port."
}
artisan() {
gosu www-data php artisan "$@"
}
render_apache_config() {
export APP_DOMAIN HLS_DOMAIN HLS_INTERNAL_PORT APP_URL
envsubst '${APP_DOMAIN} ${HLS_DOMAIN} ${HLS_INTERNAL_PORT} ${APP_URL}' \
< /etc/apache2/sites-available/nyone.conf.template \
> /etc/apache2/sites-available/000-default.conf
}
prepare_laravel_paths() {
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
}
cache_laravel_bootstrap() {
if [[ "${APP_OPTIMIZE_ON_BOOT:-true}" != "true" ]]; then
return 0
fi
artisan config:clear
artisan route:clear
artisan view:clear
artisan event:clear
artisan config:cache
artisan route:cache
artisan view:cache
}
require_env APP_KEY
require_env APP_DOMAIN
require_env HLS_DOMAIN
require_env MEDIAMTX_SHARED_SECRET
require_env DB_DATABASE
require_env DB_USERNAME
require_env DB_PASSWORD
if [[ "${WAIT_FOR_SERVICES:-true}" == "true" ]]; then
wait_for "${DB_HOST:-db}" "${DB_PORT:-5432}" "PostgreSQL"
wait_for "${REDIS_HOST:-redis}" "${REDIS_PORT:-6379}" "Redis"
if [[ "$role" == "viewer-sync" ]]; then
wait_for "${MEDIAMTX_HOST:-mediamtx}" "${MEDIAMTX_API_PORT:-9997}" "MediaMTX API"
fi
fi
prepare_laravel_paths
artisan storage:link --force >/dev/null || true
cache_laravel_bootstrap
case "$role" in
app)
render_apache_config
exec apache2-foreground
;;
migrate)
artisan migrate --force
if [[ "${SEED_DATABASE:-false}" == "true" ]]; then
artisan db:seed --force
fi
;;
queue)
exec gosu www-data php artisan queue:work "${QUEUE_CONNECTION:-database}" \
--queue="${QUEUE_NAMES:-default}" \
--sleep="${QUEUE_SLEEP:-3}" \
--tries="${QUEUE_TRIES:-3}" \
--max-time="${QUEUE_MAX_TIME:-3600}" \
--timeout="${QUEUE_TIMEOUT:-90}"
;;
scheduler)
exec gosu www-data php artisan schedule:work
;;
viewer-sync)
exec gosu www-data php artisan streaming:sync-viewer-counts \
--isolated \
--interval="${STREAMING_VIEWER_SYNC_INTERVAL:-2}"
;;
artisan)
shift || true
exec gosu www-data php artisan "$@"
;;
*)
exec "$@"
;;
esac

View File

@@ -0,0 +1,17 @@
expose_php=Off
memory_limit=512M
upload_max_filesize=100M
post_max_size=100M
max_execution_time=120
max_input_vars=3000
variables_order=EGPCS
opcache.enable=1
opcache.enable_cli=1
opcache.memory_consumption=256
opcache.interned_strings_buffer=32
opcache.max_accelerated_files=20000
opcache.validate_timestamps=0
opcache.save_comments=1
opcache.jit=0

View File

@@ -0,0 +1,17 @@
FROM bluenviron/mediamtx:latest-ffmpeg
USER root
RUN (apk add --no-cache curl gettext >/dev/null 2>&1) \
|| (apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates curl gettext-base \
&& rm -rf /var/lib/apt/lists/*)
COPY docker/mediamtx/entrypoint.sh /usr/local/bin/nyone-mediamtx-entrypoint
COPY docker/mediamtx/mediamtx.yml.template /etc/mediamtx/mediamtx.yml.template
RUN chmod +x /usr/local/bin/nyone-mediamtx-entrypoint \
&& mkdir -p /config /var/www/html/storage/app/private/recordings
ENTRYPOINT ["nyone-mediamtx-entrypoint"]

View File

@@ -0,0 +1,35 @@
#!/usr/bin/env sh
set -eu
fail() {
echo "nyone-mediamtx-entrypoint: $*" >&2
exit 1
}
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."
fi
: "${APP_URL:?APP_URL is required.}"
: "${HLS_PUBLIC_URL:?HLS_PUBLIC_URL is required.}"
: "${RTMP_INTERNAL_PORT:=19935}"
: "${HLS_INTERNAL_PORT:=19988}"
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
envsubst '${APP_URL} ${HLS_PUBLIC_URL} ${MEDIAMTX_SHARED_SECRET} ${RTMP_INTERNAL_PORT} ${HLS_INTERNAL_PORT}' \
< /etc/mediamtx/mediamtx.yml.template \
> /config/mediamtx.yml
if command -v mediamtx >/dev/null 2>&1; then
exec mediamtx /config/mediamtx.yml
fi
if [ -x /mediamtx ]; then
exec /mediamtx /config/mediamtx.yml
fi
fail "MediaMTX binary was not found in the base image."

View File

@@ -0,0 +1,35 @@
authMethod: http
authHTTPAddress: http://app/internal/mediamtx/auth?secret=${MEDIAMTX_SHARED_SECRET}
authHTTPExclude:
- action: read
- action: playback
- action: api
- action: metrics
- action: pprof
api: yes
apiAddress: :9997
rtmp: yes
rtmpAddress: :${RTMP_INTERNAL_PORT}
hls: yes
hlsAddress: :${HLS_INTERNAL_PORT}
hlsVariant: mpegts
hlsAllowOrigins:
- ${APP_URL}
- ${HLS_PUBLIC_URL}
pathDefaults:
record: yes
recordPath: /var/www/html/storage/app/private/recordings/%path/%Y-%m-%d_%H-%M-%S-%f
runOnReady: curl -fsS -X POST "http://app/internal/mediamtx/ready?secret=${MEDIAMTX_SHARED_SECRET}" --data-urlencode "path=$MTX_PATH" --data-urlencode "source_type=$MTX_SOURCE_TYPE" --data-urlencode "source_id=$MTX_SOURCE_ID"
runOnReadyRestart: no
runOnNotReady: curl -fsS -X POST "http://app/internal/mediamtx/not-ready?secret=${MEDIAMTX_SHARED_SECRET}" --data-urlencode "path=$MTX_PATH" --data-urlencode "source_type=$MTX_SOURCE_TYPE" --data-urlencode "source_id=$MTX_SOURCE_ID"
runOnRecordSegmentComplete: curl -fsS -X POST "http://app/internal/mediamtx/recording-complete?secret=${MEDIAMTX_SHARED_SECRET}" --data-urlencode "path=$MTX_PATH" --data-urlencode "segment_path=$MTX_SEGMENT_PATH" --data-urlencode "duration=$MTX_SEGMENT_DURATION"
paths:
all_others:
source: publisher

View File

@@ -0,0 +1,184 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
ENV_FILE="$ROOT_DIR/.env"
EXAMPLE_ENV_FILE="$ROOT_DIR/.env.example"
SOURCE_DIR="$ROOT_DIR/runtime/source"
COMPOSE_FILE="$ROOT_DIR/docker-compose.yml"
info() {
printf '[nyone-deploy] %s\n' "$*"
}
fail() {
printf '[nyone-deploy] ERROR: %s\n' "$*" >&2
exit 1
}
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"
}
set_env_value() {
local key="$1"
local value="$2"
local escaped
escaped="$(printf '%s' "$value" | sed 's/[\/&]/\\&/g')"
if grep -qE "^${key}=" "$ENV_FILE"; then
sed -i.bak "s/^${key}=.*/${key}=${escaped}/" "$ENV_FILE"
rm -f "$ENV_FILE.bak"
else
printf '\n%s=%s\n' "$key" "$value" >> "$ENV_FILE"
fi
}
is_missing_secret() {
local value="$1"
[ -z "$value" ] || [[ "$value" == "change-me"* ]] || [[ "$value" == "base64:change-me"* ]]
}
random_secret() {
openssl rand -base64 48 | tr -d '\n' | tr '/+' '_-' | tr -d '='
}
ensure_env_file() {
if [ -f "$ENV_FILE" ]; then
return 0
fi
[ -f "$EXAMPLE_ENV_FILE" ] || fail ".env.example was not found."
cp "$EXAMPLE_ENV_FILE" "$ENV_FILE"
info "Created .env from .env.example."
}
ensure_secret() {
local key="$1"
local value
value="$(env_value "$key")"
if is_missing_secret "$value"; then
set_env_value "$key" "$(random_secret)"
info "Generated $key."
fi
}
require_config_value() {
local key="$1"
local placeholder="${2:-}"
local value
value="$(env_value "$key")"
[ -n "$value" ] || fail "Set $key in $ENV_FILE."
if [ -n "$placeholder" ] && [ "$value" = "$placeholder" ]; then
fail "Replace the placeholder $key in $ENV_FILE."
fi
}
ensure_app_key() {
local value
value="$(env_value APP_KEY)"
if is_missing_secret "$value"; then
set_env_value APP_KEY "base64:$(openssl rand -base64 32 | tr -d '\n')"
info "Generated APP_KEY."
fi
}
clone_or_update_source() {
local repository_url="$1"
local git_ref="$2"
mkdir -p "$(dirname "$SOURCE_DIR")"
if [ -d "$SOURCE_DIR/.git" ]; then
info "Updating existing source checkout."
git -C "$SOURCE_DIR" fetch --prune --tags origin
if git -C "$SOURCE_DIR" rev-parse --verify --quiet "origin/$git_ref" >/dev/null; then
git -C "$SOURCE_DIR" checkout -B "$git_ref" "origin/$git_ref"
else
git -C "$SOURCE_DIR" checkout "$git_ref"
fi
return 0
fi
if [ -e "$SOURCE_DIR" ]; then
fail "$SOURCE_DIR exists but is not a Git checkout. Move it away or remove it before deploying."
fi
info "Cloning application repository."
git clone "$repository_url" "$SOURCE_DIR"
git -C "$SOURCE_DIR" checkout "$git_ref"
}
run_compose() {
docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" "$@"
}
require_command git
require_command docker
require_command openssl
ensure_env_file
ensure_app_key
ensure_secret POSTGRES_PASSWORD
ensure_secret MEDIAMTX_SHARED_SECRET
GIT_REPOSITORY_URL="$(env_value GIT_REPOSITORY_URL)"
GIT_REF="$(env_value GIT_REF)"
require_config_value GIT_REPOSITORY_URL "https://github.com/your-org/nyone.git"
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."
fi
GIT_REF="${GIT_REF:-main}"
clone_or_update_source "$GIT_REPOSITORY_URL" "$GIT_REF"
info "Validating Compose configuration."
run_compose config >/dev/null
info "Building Docker images."
run_compose build
info "Starting database and Redis."
run_compose up -d db redis
info "Running migrations."
run_compose --profile tools run --rm migrate
info "Starting background workers."
run_compose up -d --remove-orphans app mediamtx queue scheduler viewer-sync
info "Current container status:"
run_compose ps