diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000..0af94525e7 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,10 @@ +.git +node_modules +vendor +database/database.sqlite +storage/debugbar/*.json +storage/logs/*.log +storage/framework/cache/data/* +storage/framework/sessions/* +storage/framework/testing +storage/framework/views/*.php diff --git a/.env.example b/.env.example index 84ff1d4324..6a128b13df 100644 --- a/.env.example +++ b/.env.example @@ -4,7 +4,6 @@ APP_KEY= APP_TIMEZONE=UTC APP_URL=http://panel.test APP_LOCALE=en -APP_INSTALLED=false LOG_CHANNEL=daily LOG_STACK=single diff --git a/.github/docker/entrypoint.sh b/.github/docker/entrypoint.sh index 7f4de80cd8..14f779e573 100644 --- a/.github/docker/entrypoint.sh +++ b/.github/docker/entrypoint.sh @@ -1,81 +1,58 @@ #!/bin/ash -e -cd /app -mkdir -p /var/log/panel/logs/ /var/log/supervisord/ /var/log/nginx/ /var/log/php8/ \ - && chmod 777 /var/log/panel/logs/ \ - && ln -s /app/storage/logs/ /var/log/panel/ +#mkdir -p /var/log/supervisord/ /var/log/php8/ \ ## check for .env file and generate app keys if missing -if [ -f /app/var/.env ]; then +if [ -f /pelican-data/.env ]; then echo "external vars exist." - rm -rf /app/.env - ln -s /app/var/.env /app/ + rm -rf /var/www/html/.env else echo "external vars don't exist." - rm -rf /app/.env - touch /app/var/.env + rm -rf /var/www/html/.env + touch /pelican-data/.env ## manually generate a key because key generate --force fails if [ -z $APP_KEY ]; then echo -e "Generating key." APP_KEY=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1) echo -e "Generated app key: $APP_KEY" - echo -e "APP_KEY=$APP_KEY" > /app/var/.env + echo -e "APP_KEY=$APP_KEY" > /pelican-data/.env else echo -e "APP_KEY exists in environment, using that." - echo -e "APP_KEY=$APP_KEY" > /app/var/.env + echo -e "APP_KEY=$APP_KEY" > /pelican-data/.env fi - - ln -s /app/var/.env /app/ fi -echo "Checking if https is required." -if [ -f /etc/nginx/http.d/panel.conf ]; then - echo "Using nginx config already in place." - if [ $LE_EMAIL ]; then - echo "Checking for cert update" - certbot certonly -d $(echo $APP_URL | sed 's~http[s]*://~~g') --standalone -m $LE_EMAIL --agree-tos -n - else - echo "No letsencrypt email is set" - fi -else - echo "Checking if letsencrypt email is set." - if [ -z $LE_EMAIL ]; then - echo "No letsencrypt email is set using http config." - cp .github/docker/default.conf /etc/nginx/http.d/panel.conf - else - echo "writing ssl config" - cp .github/docker/default_ssl.conf /etc/nginx/http.d/panel.conf - echo "updating ssl config for domain" - sed -i "s||$(echo $APP_URL | sed 's~http[s]*://~~g')|g" /etc/nginx/http.d/panel.conf - echo "generating certs" - certbot certonly -d $(echo $APP_URL | sed 's~http[s]*://~~g') --standalone -m $LE_EMAIL --agree-tos -n - fi - echo "Removing the default nginx config" - rm -rf /etc/nginx/http.d/default.conf -fi +mkdir /pelican-data/database +ln -s /pelican-data/.env /var/www/html/ +ln -s /pelican-data/database/database.sqlite /var/www/html/database/ -if [[ -z $DB_PORT ]]; then - echo -e "DB_PORT not specified, defaulting to 3306" - DB_PORT=3306 +if ! grep -q "APP_KEY=" .env || grep -q "APP_KEY=$" .env; then + echo "Generating APP_KEY..." + php artisan key:generate --force +else + echo "APP_KEY is already set." fi -## check for DB up before starting the panel -echo "Checking database status." -until nc -z -v -w30 $DB_HOST $DB_PORT -do - echo "Waiting for database connection..." - # wait for 1 seconds before check again - sleep 1 -done - ## make sure the db is set up -echo -e "Migrating and Seeding D.B" -php artisan migrate --seed --force +echo -e "Migrating Database" +php artisan migrate --force ## start cronjobs for the queue echo -e "Starting cron jobs." crond -L /var/log/crond -l 5 -echo -e "Starting supervisord." +export SUPERVISORD_CADDY=false + +## disable caddy if SKIP_CADDY is set +if [[ -z $SKIP_CADDY ]]; then + echo "Starting PHP-FPM and Caddy" + export SUPERVISORD_CADDY=true +else + echo "Starting PHP-FPM only" +fi + +chown -R www-data:www-data . /pelican-data/.env /pelican-data/database + +echo "Starting Supervisord" exec "$@" diff --git a/.github/docker/supervisord.conf b/.github/docker/supervisord.conf index da6823aeb7..4d6b745c88 100644 --- a/.github/docker/supervisord.conf +++ b/.github/docker/supervisord.conf @@ -25,15 +25,15 @@ autostart=true autorestart=true [program:queue-worker] -command=/usr/local/bin/php /app/artisan queue:work --queue=high,standard,low --sleep=3 --tries=3 -user=nginx +command=/usr/local/bin/php /var/www/html/artisan queue:work --queue=high,standard,low --sleep=3 --tries=3 +user=www-data autostart=true autorestart=true -[program:nginx] -command=/usr/sbin/nginx -g 'daemon off;' -autostart=true -autorestart=true +[program:caddy] +command=caddy run --config /etc/caddy/Caddyfile --adapter caddyfile +autostart=%(ENV_SUPERVISORD_CADDY)s +autorestart=%(ENV_SUPERVISORD_CADDY)s priority=10 stdout_events_enabled=true stderr_events_enabled=true \ No newline at end of file diff --git a/Caddyfile b/Caddyfile new file mode 100644 index 0000000000..1c835bf057 --- /dev/null +++ b/Caddyfile @@ -0,0 +1,11 @@ +{ + email {$ADMIN_EMAIL} +} + +{$APP_URL} { + root * /var/www/html/public + encode gzip + + php_fastcgi 127.0.0.1:9000 + file_server +} diff --git a/Dockerfile b/Dockerfile index ef134d55bf..8c99bcc252 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,41 +1,58 @@ -# Stage 0: -# Build the assets that are needed for the frontend. This build stage is then discarded -# since we won't need NodeJS anymore in the future. This Docker image ships a final production -# level distribution -FROM --platform=$TARGETOS/$TARGETARCH node:20-alpine -WORKDIR /app -COPY . ./ -RUN yarn install --frozen-lockfile \ - && yarn run build:production +# Pelican Production Dockerfile + +FROM node:20-alpine AS yarn +#FROM --platform=$TARGETOS/$TARGETARCH node:20-alpine AS yarn + +WORKDIR /build -# Stage 1: -# Build the actual container with all of the needed PHP dependencies that will run the application. -FROM --platform=$TARGETOS/$TARGETARCH php:8.3-fpm-alpine -WORKDIR /app COPY . ./ -COPY --from=0 /app/public/assets ./public/assets -RUN apk add --no-cache --update ca-certificates dcron curl git supervisor tar unzip nginx libpng-dev libxml2-dev libzip-dev icu-dev certbot certbot-nginx \ - && docker-php-ext-configure zip \ - && docker-php-ext-install bcmath gd intl pdo_mysql zip \ - && curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer \ - && cp .env.example .env \ - && mkdir -p bootstrap/cache/ storage/logs storage/framework/sessions storage/framework/views storage/framework/cache \ - && chmod 777 -R bootstrap storage \ - && composer install --no-dev --optimize-autoloader \ - && rm -rf .env bootstrap/cache/*.php \ - && mkdir -p /app/storage/logs/ \ - && chown -R nginx:nginx . - -RUN rm /usr/local/etc/php-fpm.conf \ - && echo "* * * * * /usr/local/bin/php /app/artisan schedule:run >> /dev/null 2>&1" >> /var/spool/cron/crontabs/root \ - && echo "0 23 * * * certbot renew --nginx --quiet" >> /var/spool/cron/crontabs/root \ - && sed -i s/ssl_session_cache/#ssl_session_cache/g /etc/nginx/nginx.conf \ - && mkdir -p /var/run/php /var/run/nginx - -COPY .github/docker/default.conf /etc/nginx/http.d/default.conf -COPY .github/docker/www.conf /usr/local/etc/php-fpm.conf -COPY .github/docker/supervisord.conf /etc/supervisord.conf - -EXPOSE 80 443 + +RUN yarn install --frozen-lockfile && yarn run build:production + +FROM php:8.3-fpm-alpine +# FROM --platform=$TARGETOS/$TARGETARCH php:8.3-fpm-alpine + +COPY --from=composer:latest /usr/bin/composer /usr/local/bin/composer + +WORKDIR /var/www/html + +# Install dependencies +RUN apk update && apk add --no-cache \ + libpng-dev libjpeg-turbo-dev freetype-dev libzip-dev icu-dev \ + zip unzip curl \ + caddy ca-certificates supervisor \ + && docker-php-ext-install bcmath gd intl zip opcache pcntl posix pdo_mysql + +# Copy the Caddyfile to the container +COPY Caddyfile /etc/caddy/Caddyfile + +# Copy the application code to the container +COPY . . + +COPY --from=yarn /build/public/assets ./public/assets + +RUN touch .env + +RUN composer install --no-dev --optimize-autoloader + +# Set file permissions +RUN chmod -R 755 /var/www/html/storage \ + && chmod -R 755 /var/www/html/bootstrap/cache + +# Add scheduler to cron +RUN echo "* * * * * php /var/www/html/artisan schedule:run >> /dev/null 2>&1" | crontab -u www-data - + +## supervisord config and log dir +RUN cp .github/docker/supervisord.conf /etc/supervisord.conf && \ + mkdir /var/log/supervisord/ + +HEALTHCHECK --interval=5m --timeout=10s --start-period=5s --retries=3 \ + CMD curl -f http://localhost/up || exit 1 + +EXPOSE 80:2019 +EXPOSE 443 + +VOLUME /pelican-data + ENTRYPOINT [ "/bin/ash", ".github/docker/entrypoint.sh" ] CMD [ "supervisord", "-n", "-c", "/etc/supervisord.conf" ] diff --git a/app/Filament/Pages/Installer/PanelInstaller.php b/app/Filament/Pages/Installer/PanelInstaller.php index e588f2fe91..44be752d44 100644 --- a/app/Filament/Pages/Installer/PanelInstaller.php +++ b/app/Filament/Pages/Installer/PanelInstaller.php @@ -7,6 +7,7 @@ use App\Filament\Pages\Installer\Steps\EnvironmentStep; use App\Filament\Pages\Installer\Steps\RedisStep; use App\Filament\Pages\Installer\Steps\RequirementsStep; +use App\Models\User; use App\Services\Users\UserCreationService; use App\Traits\CheckMigrationsTrait; use App\Traits\EnvironmentWriterTrait; @@ -43,12 +44,23 @@ public function getMaxWidth(): MaxWidth|string return MaxWidth::SevenExtraLarge; } - public function mount() + public static function show(): bool { - if (is_installed()) { - abort(404); + if (User::count() <= 0) { + return true; + } + + if (config('panel.client_features.installer.enabled')) { + return true; } + return false; + } + + public function mount() + { + abort_unless(self::show(), 404); + $this->form->fill(); } @@ -122,7 +134,7 @@ public function submit() $user = app(UserCreationService::class)->handle($userData); // Install setup complete - $this->writeToEnvironment(['APP_INSTALLED' => 'true']); + $this->writeToEnvironment(['APP_INSTALLER' => 'false']); $this->rememberData(); diff --git a/app/Filament/Pages/Installer/Steps/AdminUserStep.php b/app/Filament/Pages/Installer/Steps/AdminUserStep.php index 68ebb6510e..cbd8424e70 100644 --- a/app/Filament/Pages/Installer/Steps/AdminUserStep.php +++ b/app/Filament/Pages/Installer/Steps/AdminUserStep.php @@ -16,11 +16,11 @@ public static function make(): Step ->label('Admin E-Mail') ->required() ->email() - ->default('admin@example.com'), + ->placeholder('admin@example.com'), TextInput::make('user.username') ->label('Admin Username') ->required() - ->default('admin'), + ->placeholder('admin'), TextInput::make('user.password') ->label('Admin Password') ->required() diff --git a/app/Filament/Pages/Installer/Steps/EnvironmentStep.php b/app/Filament/Pages/Installer/Steps/EnvironmentStep.php index d9cc5eafaa..b70c47f32a 100644 --- a/app/Filament/Pages/Installer/Steps/EnvironmentStep.php +++ b/app/Filament/Pages/Installer/Steps/EnvironmentStep.php @@ -23,9 +23,9 @@ class EnvironmentStep ]; public const QUEUE_DRIVERS = [ + 'sync' => 'Sync', 'database' => 'Database', 'redis' => 'Redis', - 'sync' => 'Synchronous', ]; public const DATABASE_DRIVERS = [ @@ -76,7 +76,7 @@ public static function make(): Step ToggleButtons::make('env.QUEUE_CONNECTION') ->label('Queue Driver') ->hintIcon('tabler-question-mark') - ->hintIconTooltip('The driver used for handling queues. We recommend "Database".') + ->hintIconTooltip('The driver used for handling queues. We recommend "Sync" or "Database".') ->required() ->inline() ->options(self::QUEUE_DRIVERS) diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php index e1984bc888..45e4a7729a 100644 --- a/app/Http/Controllers/Auth/LoginController.php +++ b/app/Http/Controllers/Auth/LoginController.php @@ -2,6 +2,7 @@ namespace App\Http\Controllers\Auth; +use App\Filament\Pages\Installer\PanelInstaller; use Carbon\CarbonImmutable; use Illuminate\Support\Str; use Illuminate\Http\Request; @@ -17,8 +18,12 @@ class LoginController extends AbstractLoginController * base authentication view component. React will take over at this point and * turn the login area into an SPA. */ - public function index(): View + public function index() { + if (PanelInstaller::show()) { + return redirect('/installer'); + } + return view('templates/auth.core'); } diff --git a/app/helpers.php b/app/helpers.php index 4f3e87021f..2e21fc7280 100644 --- a/app/helpers.php +++ b/app/helpers.php @@ -17,34 +17,3 @@ function is_ip(?string $address): bool return $address !== null && filter_var($address, FILTER_VALIDATE_IP) !== false; } } - -if (!function_exists('object_get_strict')) { - /** - * Get an object using dot notation. An object key with a value of null is still considered valid - * and will not trigger the response of a default value (unlike object_get). - */ - function object_get_strict(object $object, ?string $key, mixed $default = null): mixed - { - if (is_null($key) || trim($key) == '') { - return $object; - } - - foreach (explode('.', $key) as $segment) { - if (!is_object($object) || !property_exists($object, $segment)) { - return value($default); - } - - $object = $object->{$segment}; - } - - return $object; - } -} - -if (!function_exists('is_installed')) { - function is_installed(): bool - { - // This defaults to true so existing panels count as "installed" - return env('APP_INSTALLED', true); - } -} diff --git a/compose.yml b/compose.yml new file mode 100644 index 0000000000..23d2f2a397 --- /dev/null +++ b/compose.yml @@ -0,0 +1,58 @@ +x-common: + panel: + &panel-environment + APP_URL: "https://localhost" # can be set to 'http://localhost' on port 80 only + ADMIN_EMAIL: "USEYOUROWNEMAILHERE@example.com" + + APP_DEBUG: "false" + APP_ENVIRONMENT_ONLY: "false" + APP_ENV: "production" + SESSION_DRIVER: "file" + + mail: + &mail-environment + MAIL_DRIVER: "log" + # MAIL_HOST: "" + # MAIL_PORT: "" + # MAIL_FROM: "" + # MAIL_USERNAME: "" + # MAIL_PASSWORD: "" + # MAIL_ENCRYPTION: "" + +# +# ------------------------------------------------------------------------------------------ +# DANGER ZONE BELOW +# +# The remainder of this file likely does not need to be changed. Please only make modifications +# below if you understand what you are doing. +# + +services: + panel: + image: ghcr.io/pelican-dev/panel:latest + restart: always + networks: + - default + ports: + - "80:80" + - "443:443" + # - "9000:9000" # enable when not using caddy to be abel to reach php-fpm + extra_hosts: + - "host.docker.internal:host-gateway" # shows the panel on te internal docker network as well. usually '172.17.0.1' + volumes: + - pelican-data:/pelican-data + - pelican-logs:/var/www/html/storage/logs + environment: + <<: [*panel-environment, *mail-environment] + XDG_DATA_HOME: /pelican-data + # SKIP_CADDY: true # enable when not using caddy. + +volumes: + pelican-data: + pelican-logs: + +networks: + default: + ipam: + config: + - subnet: 172.20.0.0/16 diff --git a/composer.json b/composer.json index 5871186b1b..6b44652ffc 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,6 @@ "ext-json": "*", "ext-mbstring": "*", "ext-pdo": "*", - "ext-pdo_mysql": "*", "ext-zip": "*", "abdelhamiderrahmouni/filament-monaco-editor": "0.2.1", "aws/aws-sdk-php": "~3.288.1", diff --git a/config/database.php b/config/database.php index 016d1a9262..bc995eddac 100644 --- a/config/database.php +++ b/config/database.php @@ -1,5 +1,11 @@ startsWith('/')) { + $databasePath = $database; +} + return [ 'default' => env('DB_CONNECTION', 'sqlite'), @@ -8,7 +14,7 @@ 'sqlite' => [ 'driver' => 'sqlite', 'url' => env('DB_URL'), - 'database' => database_path(env('DB_DATABASE', 'database.sqlite')), + 'database' => $datapasePath, 'prefix' => '', 'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true), ], diff --git a/config/panel.php b/config/panel.php index 32b11bc3a2..62273bbbf8 100644 --- a/config/panel.php +++ b/config/panel.php @@ -74,7 +74,7 @@ | Client Features |-------------------------------------------------------------------------- | - | Allow clients to create their own databases. + | Allow clients to turn features on or off */ 'client_features' => [ @@ -93,6 +93,10 @@ 'range_start' => env('PANEL_CLIENT_ALLOCATIONS_RANGE_START'), 'range_end' => env('PANEL_CLIENT_ALLOCATIONS_RANGE_END'), ], + + 'installer' => [ + 'enabled' => env('APP_INSTALLER', true), + ], ], /*