diff --git a/.ddev/commands/redis/redis-cli b/.ddev/commands/redis/redis-cli index 27bf575b3..8824c6a6b 100755 --- a/.ddev/commands/redis/redis-cli +++ b/.ddev/commands/redis/redis-cli @@ -4,4 +4,4 @@ ## Usage: redis-cli [flags] [args] ## Example: "redis-cli KEYS *" or "ddev redis-cli INFO" or "ddev redis-cli --version" -redis-cli -p 6379 -h redis $@ +exec redis-cli -p 6379 -h redis "$@" diff --git a/.dockerignore b/.dockerignore index 70376cdf4..757a67a51 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,8 +1,30 @@ -data -Dockerfile -contrib/docker/Dockerfile.* -docker-compose*.yml -.dockerignore -.git -.gitignore -.env +.DS_Store +/.bash_history +/.bash_profile +/.bashrc +/.composer +/.env +/.env.dottie-backup +/.git +/.git-credentials +/.gitconfig +/.gitignore +/.idea +/.vagrant +/bootstrap/cache +/docker-compose-state/ +/Homestead.json +/Homestead.yaml +/node_modules +/npm-debug.log +/public/hot +/public/storage +/public/vendor/horizon +/storage/*.key +/storage/docker +/vendor +/yarn-error.log + +# Exceptions - these *MUST* be last +!/bootstrap/cache/.gitignore +!/public/vendor/horizon/.gitignore diff --git a/.editorconfig b/.editorconfig index 3c44241cc..eff249956 100644 --- a/.editorconfig +++ b/.editorconfig @@ -7,3 +7,21 @@ end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true + +[*.{yml,yaml}] +indent_style = space +indent_size = 2 + +[*.{sh,envsh,env,env*}] +indent_style = space +indent_size = 4 + +# ShellCheck config +shell_variant = bash # like -ln=bash +binary_next_line = true # like -bn +switch_case_indent = true # like -ci +space_redirects = false # like -sr +keep_padding = false # like -kp +function_next_line = true # like -fn +never_split = true # like -ns +simplify = true diff --git a/.env.docker b/.env.docker index ce4cfe87c..0d5836745 100644 --- a/.env.docker +++ b/.env.docker @@ -1,149 +1,1278 @@ -## Crypto +#!/bin/bash +# -*- mode: bash -*- +# vi: ft=bash +# shellcheck disable=SC2034,SC2148 + +# Use Dottie (https://github.com/jippi/dottie) to manage this .env file easier! +# +# For example: +# +# Run [dottie update] to update your [.env] file with upstream (as part of upgrade) +# Run [dottie validate] to validate youe [.env] file +# +# @dottie/source .env.docker + +################################################################################ +# app +################################################################################ + +# The name/title for your site +# @see https://docs.pixelfed.org/technical-documentation/config/#app_name-1 +# @dottie/example My Pixelfed Site +# @dottie/validate required,ne=My Pixelfed Site +APP_NAME= + +# Application domain used for routing. (e.g., pixelfed.org) +# +# @see https://docs.pixelfed.org/technical-documentation/config/#app_domain +# @dottie/example example.com +# @dottie/validate required,ne=example.com,fqdn +APP_DOMAIN="example.com" + +# This URL is used by the console to properly generate URLs when using the Artisan command line tool. +# You should set this to the root of your application so that it is used when running Artisan tasks. +# +# @see https://docs.pixelfed.org/technical-documentation/config/#app_url +# @dottie/validate required,http_url +APP_URL="https://${APP_DOMAIN}" + +# Application domains used for routing. +# +# @see https://docs.pixelfed.org/technical-documentation/config/#admin_domain +# @dottie/validate required,fqdn +ADMIN_DOMAIN="${APP_DOMAIN}" + +# This value determines the “environment” your application is currently running in. +# This may determine how you prefer to configure various services your application utilizes. +# +# @default "production" +# @see https://docs.pixelfed.org/technical-documentation/config/#app_env +# @dottie/validate required,oneof=production dev staging +#APP_ENV="production" + +# When your application is in debug mode, detailed error messages with stack traces will +# be shown on every error that occurs within your application. +# +# If disabled, a simple generic error page is shown. +# +# @default "false" +# @see https://docs.pixelfed.org/technical-documentation/config/#app_debug +# @dottie/validate required,boolean +#APP_DEBUG="false" + +# Enable/disable new local account registrations. +# +# @default "true" +# @see https://docs.pixelfed.org/technical-documentation/config/#open_registration +# @dottie/validate required,boolean +#OPEN_REGISTRATION="true" + +# Require email verification before a new user can do anything. +# +# @default "true" +# @see https://docs.pixelfed.org/technical-documentation/config/#enforce_email_verification +# @dottie/validate required,boolean +#ENFORCE_EMAIL_VERIFICATION="true" + +# Allow a maximum number of user accounts. +# +# @default "1000" +# @see https://docs.pixelfed.org/technical-documentation/config/#pf_max_users +# @dottie/validate required,number +#PF_MAX_USERS="1000" + +# Enforce the maximum number of user accounts +# +# @default "true" +# @dottie/validate boolean +#PF_ENFORCE_MAX_USERS="true" + +# @default "false" +# @see https://docs.pixelfed.org/technical-documentation/config/#oauth_enabled +# @dottie/validate required,boolean +#OAUTH_ENABLED="false" + +# ! Do not edit your timezone once the service is running - or things will break! +# +# @default "UTC" +# @see https://docs.pixelfed.org/technical-documentation/config/#app_timezone +# @see https://www.php.net/manual/en/timezones.php +# @dottie/validate required,timezone +APP_TIMEZONE="UTC" + +# The application locale determines the default locale that will be used by the translation service provider. +# You are free to set this value to any of the locales which will be supported by the application. +# +# @default "en" +# @see https://docs.pixelfed.org/technical-documentation/config/#app_locale +# @dottie/validate required +#APP_LOCALE="en" + +# The fallback locale determines the locale to use when the current one is not available. +# +# You may change the value to correspond to any of the language folders that are provided through your application. +# +# @default "en" +# @see https://docs.pixelfed.org/technical-documentation/config/#app_fallback_locale +# @dottie/validate required +#APP_FALLBACK_LOCALE="en" + +# @see https://docs.pixelfed.org/technical-documentation/config/#limit_account_size +# @dottie/validate required,boolean +#LIMIT_ACCOUNT_SIZE="true" + +# Update the max account size, the per user limit of files in kB. +# +# @default "1000000" (1GB) +# @see https://docs.pixelfed.org/technical-documentation/config/#max_account_size-kb +# @dottie/validate required,number +#MAX_ACCOUNT_SIZE="1000000" + +# Update the max photo size, in kB. +# +# @default "15000" (15MB) +# @see https://docs.pixelfed.org/technical-documentation/config/#max_photo_size-kb +# @dottie/validate required,number +#MAX_PHOTO_SIZE="15000" + +# The max number of photos allowed per post. +# +# @default "4" +# @see https://docs.pixelfed.org/technical-documentation/config/#max_album_length +# @dottie/validate required,number +#MAX_ALBUM_LENGTH="4" + +# Update the max avatar size, in kB. +# +# @default "2000" (2MB). +# @see https://docs.pixelfed.org/technical-documentation/config/#max_avatar_size-kb +# @dottie/validate required,number +#MAX_AVATAR_SIZE="2000" + +# Change the caption length limit for new local posts. +# +# @default "500" +# @see https://docs.pixelfed.org/technical-documentation/config/#max_caption_length +# @dottie/validate required,number +#MAX_CAPTION_LENGTH="500" + +# Change the bio length limit for user profiles. +# +# @default "125" +# @see https://docs.pixelfed.org/technical-documentation/config/#max_bio_length +# @dottie/validate required,number +#MAX_BIO_LENGTH="125" + +# Change the length limit for user names. +# +# @default "30" +# @see https://docs.pixelfed.org/technical-documentation/config/#max_name_length +# @dottie/validate required,number +#MAX_NAME_LENGTH="30" + +# Resize and optimize image uploads. +# +# @default "true" +# @see https://docs.pixelfed.org/technical-documentation/config/#pf_optimize_images +# @dottie/validate required,boolean +#PF_OPTIMIZE_IMAGES="true" + +# Set the image optimization quality, must be a value between 1-100. +# +# @default "80" +# @see https://docs.pixelfed.org/technical-documentation/config/#image_quality +# @dottie/validate required,number +#IMAGE_QUALITY="80" + +# Resize and optimize video uploads. +# +# @default "true" +# @see https://docs.pixelfed.org/technical-documentation/config/#pf_optimize_videos +# @dottie/validate required,boolean +#PF_OPTIMIZE_VIDEOS="true" + +# Enable account deletion. +# +# @default "true" +# @see https://docs.pixelfed.org/technical-documentation/config/#account_deletion +# @dottie/validate required,boolean +#ACCOUNT_DELETION="true" + +# Set account deletion queue after X days, set to false to delete accounts immediately. +# +# @default "false" +# @see https://docs.pixelfed.org/technical-documentation/config/#account_delete_after +# @dottie/validate required,boolean|number +#ACCOUNT_DELETE_AFTER="false" + +# @default "Pixelfed - Photo sharing for everyone" +# @see https://docs.pixelfed.org/technical-documentation/config/#instance_description +# @dottie/validate required +#INSTANCE_DESCRIPTION="" + +# @default "false" +# @see https://docs.pixelfed.org/technical-documentation/config/#instance_public_hashtags +# @dottie/validate required,boolean +#INSTANCE_PUBLIC_HASHTAGS="false" + +# The public e-mail address people can use to contact you by +# +# @default "" +# @see https://docs.pixelfed.org/technical-documentation/config/#instance_contact_email +# @dottie/validate required,ne=__CHANGE_ME__,email +INSTANCE_CONTACT_EMAIL="__CHANGE_ME__" + +# @default "false" +# @see https://docs.pixelfed.org/technical-documentation/config/#instance_public_local_timeline +# @dottie/validate required,boolean +#INSTANCE_PUBLIC_LOCAL_TIMELINE="false" + +# @default "" +# @see https://docs.pixelfed.org/technical-documentation/config/#banned_usernames +#BANNED_USERNAMES="" + +# @default "false" +# @see https://docs.pixelfed.org/technical-documentation/config/#stories_enabled +# @dottie/validate required,boolean +#STORIES_ENABLED="false" + +# Level is hardcoded to 1. +# +# @default "false" +# @see https://docs.pixelfed.org/technical-documentation/config/#restricted_instance +# @dottie/validate required,boolean +#RESTRICTED_INSTANCE="false" + +# @default false +# @see https://docs.pixelfed.org/technical-documentation/config/#media_exif_database +# @dottie/validate required,boolean +#MEDIA_EXIF_DATABASE="false" + +# Pixelfed supports GD or ImageMagick to process images. +# +# Possible values: +# - "gd" (default) +# - "imagick" +# +# @default "gd" +# @see https://docs.pixelfed.org/technical-documentation/config/#image_driver +# @dottie/validate required,oneof=gd imagick +#IMAGE_DRIVER="gd" + +# Set trusted proxy IP addresses. +# +# Both IPv4 and IPv6 addresses are supported, along with CIDR notation. +# +# The “*” character is syntactic sugar within TrustedProxy to trust any +# proxy that connects directly to your server, a requirement when you cannot +# know the address of your proxy (e.g. if using Rackspace balancers). +# +# The “**” character is syntactic sugar within TrustedProxy to trust not just any +# proxy that connects directly to your server, but also proxies that connect to those proxies, +# and all the way back until you reach the original source IP. It will mean that +# $request->getClientIp() always gets the originating client IP, no matter how many proxies +# that client’s request has subsequently passed through. +# +# @default "*" +# @see https://docs.pixelfed.org/technical-documentation/config/#trust_proxies +# @dottie/validate required +#TRUST_PROXIES="*" + +# This option controls the default cache connection that gets used while using this caching library. +# +# This connection is used when another is not explicitly specified when executing a given caching function. +# +# Possible values: +# - "apc" +# - "array" +# - "database" +# - "file" (default) +# - "memcached" +# - "redis" +# +# @default "file" +# @see https://docs.pixelfed.org/technical-documentation/config/#cache_driver +# @dottie/validate required,oneof=apc array database file memcached redis +CACHE_DRIVER="redis" + +# @default ${APP_NAME}_cache, or laravel_cache if no APP_NAME is set. +# @see https://docs.pixelfed.org/technical-documentation/config/#cache_prefix +# @dottie/validate required +#CACHE_PREFIX="{APP_NAME}_cache" + +# This option controls the default broadcaster that will be used by the framework when an event needs to be broadcast. +# +# Possible values: +# - "pusher" +# - "redis" +# - "log" +# - "null" (default) +# +# @default null +# @see https://docs.pixelfed.org/technical-documentation/config/#broadcast_driver +# @dottie/validate required,oneof=pusher redis log null +BROADCAST_DRIVER="redis" + +# @default "true" +# @see https://docs.pixelfed.org/technical-documentation/config/#restrict_html_types +# @dottie/validate required,boolean +#RESTRICT_HTML_TYPES="true" + +# Passport uses encryption keys while generating secure access tokens +# for your application. +# +# By default, the keys are stored as local files but can be set via environment +# variables when that is more convenient. + +# @see https://docs.pixelfed.org/technical-documentation/config/#passport_private_key +# @dottie/validate required +#PASSPORT_PRIVATE_KEY="" + +# @see https://docs.pixelfed.org/technical-documentation/config/#passport_public_key +# @dottie/validate required +#PASSPORT_PUBLIC_KEY="" + +################################################################################ +# database +################################################################################ + +# Database version to use (as Docker tag) +# +# @see https://hub.docker.com/_/mariadb +# @dottie/validate required +DB_VERSION="11.2" + +# Here you may specify which of the database connections below +# you wish to use as your default connection for all database work. +# +# Of course you may use many connections at once using the database library. +# +# Possible values: +# +# - "sqlite" +# - "mysql" (default) +# - "pgsql" +# - "sqlsrv" +# +# @see https://docs.pixelfed.org/technical-documentation/config/#db_connection +# @dottie/validate required,oneof=sqlite mysql pgsql sqlsrv +DB_CONNECTION="mysql" + +# @see https://docs.pixelfed.org/technical-documentation/config/#db_host +# @dottie/validate required,hostname +DB_HOST="db" + +# @see https://docs.pixelfed.org/technical-documentation/config/#db_username +# @dottie/validate required +DB_USERNAME="pixelfed" + +# The password to your database. Please make it secure. +# Use a site like https://pwgen.io/ to generate it +# +# @see https://docs.pixelfed.org/technical-documentation/config/#db_password +# @dottie/validate required +DB_PASSWORD= + +# @see https://docs.pixelfed.org/technical-documentation/config/#db_database +# @dottie/validate required +DB_DATABASE="pixelfed_prod" + +# Use "3306" for MySQL/MariaDB and "5432" for PostgreeSQL +# +# @see https://docs.pixelfed.org/technical-documentation/config/#db_port +# @dottie/validate required,number +DB_PORT="3306" + +# Automatically run [artisan migrate --force] if new migrations are detected. +# @dottie/validate required,boolean +DB_APPLY_NEW_MIGRATIONS_AUTOMATICALLY="false" + +################################################################################ +# mail +################################################################################ + +# Laravel supports both SMTP and PHP’s “mail” function as drivers for the sending of e-mail. +# You may specify which one you’re using throughout your application here. +# +# Possible values: +# +# "smtp" (default) +# "sendmail" +# "mailgun" +# "mandrill" +# "ses" +# "sparkpost" +# "log" +# "array" +# +# @default "smtp" +# @see https://docs.pixelfed.org/technical-documentation/config/#mail_driver +# @dottie/validate required,oneof=smtp sendmail mailgun mandrill ses sparkpost log array +#MAIL_DRIVER="smtp" + +# The host address of the SMTP server used by your applications. +# +# A default option is provided that is compatible with the Mailgun mail service which will provide reliable deliveries. +# +# @default "smtp.mailgun.org" +# @see https://docs.pixelfed.org/technical-documentation/config/#mail_host +# @dottie/validate required_with=MAIL_DRIVER,fqdn +#MAIL_HOST="smtp.mailgun.org" + +# This is the SMTP port used by your application to deliver e-mails to users of the application. +# +# Like the host we have set this value to stay compatible with the Mailgun e-mail application by default. +# +# @default 587. +# @see https://docs.pixelfed.org/technical-documentation/config/#mail_port +# @dottie/validate required_with=MAIL_DRIVER,number +#MAIL_PORT="587" + +# Here, you may specify a name and address that is used globally for all e-mails that are sent by your application. +# +# You may wish for all e-mails sent by your application to be sent from the same address. +# +# @default "bot@example.com" +# @see https://docs.pixelfed.org/technical-documentation/config/#mail_from_address +# @dottie/validate required_with=MAIL_DRIVER,email,ne=__CHANGE_ME__ +#MAIL_FROM_ADDRESS="__CHANGE_ME__" + +# The 'name' you send e-mail from +# +# @default "Example" +# @see https://docs.pixelfed.org/technical-documentation/config/#mail_from_name +# @dottie/validate required_with=MAIL_DRIVER +#MAIL_FROM_NAME="${APP_NAME}" + +# If your SMTP server requires a username for authentication, you should set it here. +# +# This will get used to authenticate with your server on connection. +# You may also set the “password” value below this one. +# +# @default "" +# @see https://docs.pixelfed.org/technical-documentation/config/#mail_username +# @dottie/validate required_with=MAIL_DRIVER +#MAIL_USERNAME="" + +# @default "" +# @see https://docs.pixelfed.org/technical-documentation/config/#mail_password +# @dottie/validate required_with=MAIL_DRIVER +#MAIL_PASSWORD="" + +# Here you may specify the encryption protocol that should be used when the application send e-mail messages. +# +# A sensible default using the transport layer security protocol should provide great security. +# +# @default "tls" +# @see https://docs.pixelfed.org/technical-documentation/config/#mail_encryption +# @dottie/validate required_with=MAIL_DRIVER +#MAIL_ENCRYPTION="tls" + +################################################################################ +# redis +################################################################################ + +# @default "phpredis" +# @see https://docs.pixelfed.org/technical-documentation/config/#redis_client +# @dottie/validate required +#REDIS_CLIENT="phpredis" + +# @default "tcp" +# @see https://docs.pixelfed.org/technical-documentation/config/#redis_scheme +# @dottie/validate required +#REDIS_SCHEME="tcp" + +# @default "localhost" +# @see https://docs.pixelfed.org/technical-documentation/config/#redis_host +# @dottie/validate required +REDIS_HOST="redis" + +# @default "null" (not set/commented out). +# @see https://docs.pixelfed.org/technical-documentation/config/#redis_password +# @dottie/validate omitempty +#REDIS_PASSWORD= + +# @default "6379" +# @see https://docs.pixelfed.org/technical-documentation/config/#redis_port +# @dottie/validate required,number +REDIS_PORT="6379" + +# @default "0" +# @see https://docs.pixelfed.org/technical-documentation/config/#redis_database +# @dottie/validate required,number +#REDIS_DATABASE="0" + +################################################################################ +# experiments +################################################################################ + +# Text only posts (alpha). +# +# @default "false" +# @see https://docs.pixelfed.org/technical-documentation/config/#exp_top +# @dottie/validate required,boolean +#EXP_TOP="false" + +# Poll statuses (alpha). +# +# @default "false" +# @see https://docs.pixelfed.org/technical-documentation/config/#exp_polls +# @dottie/validate required,boolean +#EXP_POLLS="false" + +# Cached public timeline for larger instances (beta). +# +# @default "false" +# @see https://docs.pixelfed.org/technical-documentation/config/#exp_cpt +# @dottie/validate required,boolean +#EXP_CPT="false" + +# Enforce Mastodon API Compatibility (alpha). +# +# @default "true" +# @see https://docs.pixelfed.org/technical-documentation/config/#exp_emc +# @dottie/validate required,boolean +#EXP_EMC="true" + +################################################################################ +# ActivityPub +################################################################################ + +# @default "false" +# @see https://docs.pixelfed.org/technical-documentation/config/#activity_pub +# @dottie/validate required,boolean +#ACTIVITY_PUB="true" + +# @default "true" +# @see https://docs.pixelfed.org/technical-documentation/config/#ap_remote_follow +# @dottie/validate required,boolean +#AP_REMOTE_FOLLOW="true" + +# @default "true" +# @see https://docs.pixelfed.org/technical-documentation/config/#ap_sharedinbox +# @dottie/validate required,boolean +#AP_SHAREDINBOX="true" + +# @default "true" +# @see https://docs.pixelfed.org/technical-documentation/config/#ap_inbox +# @dottie/validate required,boolean +#AP_INBOX="true" + +# @default "true" +# @see https://docs.pixelfed.org/technical-documentation/config/#ap_outbox +# @dottie/validate required,boolean +#AP_OUTBOX="true" + +################################################################################ +# Federation +################################################################################ + +# @default "true" +# @see https://docs.pixelfed.org/technical-documentation/config/#atom_feeds +# @dottie/validate required,boolean +#ATOM_FEEDS="true" + +# @default "true" +# @see https://docs.pixelfed.org/technical-documentation/config/#nodeinfo +# @dottie/validate required,boolean +#NODEINFO="true" + +# @default "true" +# @see https://docs.pixelfed.org/technical-documentation/config/#webfinger +# @dottie/validate required,boolean +#WEBFINGER="true" + +################################################################################ +# Storage +################################################################################ + +# Store media on object storage like S3, Digital Ocean Spaces, Rackspace +# +# @default "false" +# @see https://docs.pixelfed.org/technical-documentation/config/#pf_enable_cloud +# @dottie/validate required,boolean +#PF_ENABLE_CLOUD="false" + +# Many applications store files both locally and in the cloud. +# +# For this reason, you may specify a default “cloud” driver here. +# This driver will be bound as the Cloud disk implementation in the container. +# +# @default "s3" +# @see https://docs.pixelfed.org/technical-documentation/config/#filesystem_cloud +# @dottie/validate required_with=PF_ENABLE_CLOUD +#FILESYSTEM_CLOUD="s3" + +# @default true. +# @see https://docs.pixelfed.org/technical-documentation/config/#media_delete_local_after_cloud +# @dottie/validate required_with=PF_ENABLE_CLOUD,boolean +#MEDIA_DELETE_LOCAL_AFTER_CLOUD="true" + +# @see https://docs.pixelfed.org/technical-documentation/config/#aws_access_key_id +# @dottie/validate required_if=FILESYSTEM_CLOUD s3 +#AWS_ACCESS_KEY_ID="" + +# @see https://docs.pixelfed.org/technical-documentation/config/#aws_secret_access_key +# @dottie/validate required_if=FILESYSTEM_CLOUD s3 +#AWS_SECRET_ACCESS_KEY="" + +# @see https://docs.pixelfed.org/technical-documentation/config/#aws_default_region +# @dottie/validate required_if=FILESYSTEM_CLOUD s3 +#AWS_DEFAULT_REGION="" + +# @see https://docs.pixelfed.org/technical-documentation/config/#aws_bucket +# @dottie/validate required_if=FILESYSTEM_CLOUD s3 +#AWS_BUCKET="" + +# @see https://docs.pixelfed.org/technical-documentation/config/#aws_url +# @dottie/validate required_if=FILESYSTEM_CLOUD s3 +#AWS_URL="" + +# @see https://docs.pixelfed.org/technical-documentation/config/#aws_endpoint +# @dottie/validate required_if=FILESYSTEM_CLOUD s3 +#AWS_ENDPOINT="" + +# @see https://docs.pixelfed.org/technical-documentation/config/#aws_use_path_style_endpoint +# @dottie/validate required_if=FILESYSTEM_CLOUD s3 +#AWS_USE_PATH_STYLE_ENDPOINT="false" + +################################################################################ +# COSTAR +################################################################################ + +# Comma-separated list of domains to block. +# +# @default null (not set/commented out). +# @see https://docs.pixelfed.org/technical-documentation/config/#cs_blocked_domains +# @dottie/validate +#CS_BLOCKED_DOMAINS="" + +# Comma-separated list of domains to add warnings. +# +# @default null (not set/commented out). +# @see https://docs.pixelfed.org/technical-documentation/config/#cs_cw_domains +# @dottie/validate +#CS_CW_DOMAINS="" + +# Comma-separated list of domains to remove from public timelines. +# +# @default null (not set/commented out). +# @see https://docs.pixelfed.org/technical-documentation/config/#cs_unlisted_domains +# @dottie/validate +#CS_UNLISTED_DOMAINS="" + +# Comma-separated list of keywords to block. +# +# @default null (not set/commented out). +# @see https://docs.pixelfed.org/technical-documentation/config/#cs_blocked_keywords +# @dottie/validate +#CS_BLOCKED_KEYWORDS="" + +# Comma-separated list of keywords to add warnings. +# +# @default null (not set/commented out). +# @see https://docs.pixelfed.org/technical-documentation/config/#cs_cw_keywords +# @dottie/validate +#CS_CW_KEYWORDS="" + +# Comma-separated list of keywords to remove from public timelines. +# +# @default null (not set/commented out). +# @see https://docs.pixelfed.org/technical-documentation/config/#cs_unlisted_keywords +# @dottie/validate +#CS_UNLISTED_KEYWORDS="" + +# @default null (not set/commented out). +# @see https://docs.pixelfed.org/technical-documentation/config/#cs_blocked_actor +# @dottie/validate +#CS_BLOCKED_ACTOR="" + +# @default null (not set/commented out). +# @see https://docs.pixelfed.org/technical-documentation/config/#cs_cw_actor +# @dottie/validate +#CS_CW_ACTOR="" + +# @default null (not set/commented out). +# @see https://docs.pixelfed.org/technical-documentation/config/#cs_unlisted_actor +# @dottie/validate +#CS_UNLISTED_ACTOR="" + +################################################################################ +# logging +################################################################################ + +# Possible values: +# +# - "stack" (default) +# - "single" +# - "daily" +# - "slack" +# - "stderr" +# - "syslog" +# - "errorlog" +# - "null" +# - "emergency" +# - "media" +# +# @default "stack" +# @dottie/validate required,oneof=stack single daily slack stderr syslog errorlog null emergency media +LOG_CHANNEL="stderr" + +# Used by single, stderr and syslog. +# +# @default "debug" +# @see https://docs.pixelfed.org/technical-documentation/config/#log_level +# @dottie/validate required,oneof=debug info notice warning error critical alert emergency +#LOG_LEVEL="debug" + +# Used by stderr. +# +# @default "" +# @see https://docs.pixelfed.org/technical-documentation/config/#log_stderr_formatter +#LOG_STDERR_FORMATTER="" + +# Used by slack. +# +# @default "" +# @see https://docs.pixelfed.org/technical-documentation/config/#log_slack_webhook_url +# @dottie/validate required,http_url +#LOG_SLACK_WEBHOOK_URL="" + +################################################################################ +# queue +################################################################################ + +# Possible values: +# - "sync" (default) +# - "database" +# - "beanstalkd" +# - "sqs" +# - "redis" +# - "null" +# +# @default "sync" +# @see https://docs.pixelfed.org/technical-documentation/config/#queue_driver +# @dottie/validate required,oneof=sync database beanstalkd sqs redis null +QUEUE_DRIVER="redis" + +# @default "your-public-key" +# @see https://docs.pixelfed.org/technical-documentation/config/#sqs_key +# @dottie/validate required_if=QUEUE_DRIVER sqs +#SQS_KEY="your-public-key" + +# @default "your-secret-key" +# @see https://docs.pixelfed.org/technical-documentation/config/#sqs_secret +# @dottie/validate required_if=QUEUE_DRIVER sqs +#SQS_SECRET="your-secret-key" + +# @default "https://sqs.us-east-1.amazonaws.com/your-account-id" +# @see https://docs.pixelfed.org/technical-documentation/config/#sqs_prefix +# @dottie/validate required_if=QUEUE_DRIVER sqs +#SQS_PREFIX="" + +# @default "your-queue-name" +# @see https://docs.pixelfed.org/technical-documentation/config/#sqs_queue +# @dottie/validate required_if=QUEUE_DRIVER sqs +#SQS_QUEUE="your-queue-name" + +# @default "us-east-1" +# @see https://docs.pixelfed.org/technical-documentation/config/#sqs_region +# @dottie/validate required_if=QUEUE_DRIVER sqs +#SQS_REGION="us-east-1" + +################################################################################ +# session +################################################################################ + +# This option controls the default session “driver” that will be used on requests. +# +# By default, we will use the lightweight native driver but you may specify any of the other wonderful drivers provided here. +# +# Possible values: +# - "file" +# - "cookie" +# - "database" (default) +# - "apc" +# - "memcached" +# - "redis" +# - "array" +# +# @default "database" +# @dottie/validate required,oneof=file cookie database apc memcached redis array +SESSION_DRIVER="redis" + +# Here you may specify the number of minutes that you wish the session to be allowed to remain idle before it expires. +# +# If you want them to immediately expire on the browser closing, set that option. +# +# @default 86400. +# @see https://docs.pixelfed.org/technical-documentation/config/#session_lifetime +# @dottie/validate required,number +#SESSION_LIFETIME="86400" + +# Here you may change the domain of the cookie used to identify a session in your application. +# +# This will determine which domains the cookie is available to in your application. +# +# A sensible default has been set. +# +# @default the value of APP_DOMAIN, or null. +# @see https://docs.pixelfed.org/technical-documentation/config/#session_domain +# @dottie/validate required,hostname +#SESSION_DOMAIN="${APP_DOMAIN}" + +################################################################################ +# horizon +################################################################################ + +# This prefix will be used when storing all Horizon data in Redis. +# +# You may modify the prefix when you are running multiple installations +# of Horizon on the same server so that they don’t have problems. +# +# @default "horizon-" +# @see https://docs.pixelfed.org/technical-documentation/config/#horizon_prefix +# @dottie/validate required +#HORIZON_PREFIX="horizon-" + +# @default "false" +# @see https://docs.pixelfed.org/technical-documentation/config/#horizon_darkmode +# @dottie/validate required,boolean +#HORIZON_DARKMODE="false" + +# This value (in MB) describes the maximum amount of memory (in MB) the Horizon worker +# may consume before it is terminated and restarted. +# +# You should set this value according to the resources available to your server. +# +# @default "64" +# @dottie/validate required,number +#HORIZON_MEMORY_LIMIT="64" + +# @see https://docs.pixelfed.org/technical-documentation/config/#horizon_balance_strategy +# @dottie/validate required +#HORIZON_BALANCE_STRATEGY="auto" + +# @see https://docs.pixelfed.org/technical-documentation/config/#horizon_min_processes +# @dottie/validate required,number +#HORIZON_MIN_PROCESSES="1" + +# @see https://docs.pixelfed.org/technical-documentation/config/#horizon_max_processes +# @dottie/validate required,number +#HORIZON_MAX_PROCESSES="20" + +# @see https://docs.pixelfed.org/technical-documentation/config/#horizon_supervisor_memory +# @dottie/validate required,number +#HORIZON_SUPERVISOR_MEMORY="64" + +# @see https://docs.pixelfed.org/technical-documentation/config/#horizon_supervisor_tries +# @dottie/validate required,number +#HORIZON_SUPERVISOR_TRIES="3" + +# @see https://docs.pixelfed.org/technical-documentation/config/#horizon_supervisor_nice +# @dottie/validate required,number +#HORIZON_SUPERVISOR_NICE="0" + +# @see https://docs.pixelfed.org/technical-documentation/config/#horizon_supervisor_timeout +# @dottie/validate required,number +#HORIZON_SUPERVISOR_TIMEOUT="300" + +################################################################################ +# docker shared +################################################################################ + +# A random 32-character string to be used as an encryption key. +# +# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +# ! NOTE: This will be auto-generated by Docker during bootstrap +# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +# +# This key is used by the Illuminate encrypter service and should be set to a random, +# 32 character string, otherwise these encrypted strings will not be safe. +# +# @see https://docs.pixelfed.org/technical-documentation/config/#app_key +# @dottie/validate required APP_KEY= -## General Settings -APP_NAME="Pixelfed Prod" -APP_ENV=production -APP_DEBUG=false -APP_URL=https://real.domain -APP_DOMAIN="real.domain" -ADMIN_DOMAIN="real.domain" -SESSION_DOMAIN="real.domain" +# Prefix for container names (without any dash at the end) +# @dottie/validate required +DOCKER_ALL_CONTAINER_NAME_PREFIX="${APP_DOMAIN}" -OPEN_REGISTRATION=true -ENFORCE_EMAIL_VERIFICATION=false -PF_MAX_USERS=1000 -OAUTH_ENABLED=true +# How often Docker health check should run for all services +# +# Can be overridden by individual [DOCKER_*_HEALTHCHECK_INTERVAL] settings further down +# +# @default "10s" +# @dottie/validate required +DOCKER_ALL_DEFAULT_HEALTHCHECK_INTERVAL="10s" -APP_TIMEZONE=UTC -APP_LOCALE=en +# Path (relative to the docker-compose.yml) or absolute (/some/other/path) where containers will *all* data +# will be stored (data, config, overrides) +# +# @default "./docker-compose-state" +# @dottie/validate required,dir +DOCKER_ALL_HOST_ROOT_PATH="./docker-compose-state" -## Pixelfed Tweaks -LIMIT_ACCOUNT_SIZE=true -MAX_ACCOUNT_SIZE=1000000 -MAX_PHOTO_SIZE=15000 -MAX_AVATAR_SIZE=2000 -MAX_CAPTION_LENGTH=500 -MAX_BIO_LENGTH=125 -MAX_NAME_LENGTH=30 -MAX_ALBUM_LENGTH=4 -IMAGE_QUALITY=80 -PF_OPTIMIZE_IMAGES=true -PF_OPTIMIZE_VIDEOS=true -ADMIN_ENV_EDITOR=false -ACCOUNT_DELETION=true -ACCOUNT_DELETE_AFTER=false -MAX_LINKS_PER_POST=0 +# Path (relative to the docker-compose.yml) or absolute (/some/other/path) where containers will store their data +# +# @default "${DOCKER_ALL_HOST_ROOT_PATH}/data" +# @dottie/validate required,dir +DOCKER_ALL_HOST_DATA_ROOT_PATH="${DOCKER_ALL_HOST_ROOT_PATH:?error}/data" -## Instance -#INSTANCE_DESCRIPTION= -INSTANCE_PUBLIC_HASHTAGS=false -#INSTANCE_CONTACT_EMAIL= -INSTANCE_PUBLIC_LOCAL_TIMELINE=false -#BANNED_USERNAMES= -STORIES_ENABLED=false -RESTRICTED_INSTANCE=false +# Path (relative to the docker-compose.yml) or absolute (/some/other/path) where containers will store their confguration +# +# @default "${DOCKER_ALL_HOST_ROOT_PATH}/config" +# @dottie/validate required,dir +DOCKER_ALL_HOST_CONFIG_ROOT_PATH="${DOCKER_ALL_HOST_ROOT_PATH:?error}/config" -## Mail -MAIL_DRIVER=log -MAIL_HOST=smtp.mailtrap.io -MAIL_PORT=2525 -MAIL_FROM_ADDRESS="pixelfed@example.com" -MAIL_FROM_NAME="Pixelfed" -MAIL_USERNAME=null -MAIL_PASSWORD=null -MAIL_ENCRYPTION=null +# Path (relative to the docker-compose.yml) or absolute (/some/other/path) where containers will store overrides +# +# @default "${DOCKER_ALL_HOST_ROOT_PATH}/overrides" +# @dottie/validate required,dir +DOCKER_APP_HOST_OVERRIDES_PATH="${DOCKER_ALL_HOST_ROOT_PATH:?error}/overrides" -## Databases (MySQL) -DB_CONNECTION=mysql -DB_DATABASE=pixelfed_prod -DB_HOST=db -DB_PASSWORD=pixelfed_db_pass -DB_PORT=3306 -DB_USERNAME=pixelfed -# pass the same values to the db itself -MYSQL_DATABASE=pixelfed_prod -MYSQL_PASSWORD=pixelfed_db_pass -MYSQL_RANDOM_ROOT_PASSWORD=true -MYSQL_USER=pixelfed +# Set timezone used by *all* containers - these must be in sync. +# +# ! Do not edit your timezone once the service is running - or things will break! +# +# @see https://www.php.net/manual/en/timezones.php +# @dottie/validate required,timezone +TZ="${APP_TIMEZONE}" -## Databases (Postgres) -#DB_CONNECTION=pgsql -#DB_HOST=postgres -#DB_PORT=5432 -#DB_DATABASE=pixelfed -#DB_USERNAME=postgres -#DB_PASSWORD=postgres +################################################################################ +# docker app +################################################################################ -## Cache (Redis) -REDIS_CLIENT=phpredis -REDIS_SCHEME=tcp -REDIS_HOST=redis -REDIS_PASSWORD=redis_password -REDIS_PORT=6379 -REDIS_DATABASE=0 +# The docker tag prefix to use for pulling images, can be one of +# +# * latest +# * +# * staging +# * edge +# * branch- +# * pr- +# +# Combined with [DOCKER_APP_RUNTIME] and [PHP_VERSION] configured +# elsewhere in this file, the final Docker tag is computed. +# @dottie/validate required +DOCKER_APP_RELEASE="branch-jippi-fork" -HORIZON_PREFIX="horizon-" +# The PHP version to use for [web] and [worker] container +# +# Any version published on https://hub.docker.com/_/php should work +# +# Example: +# +# * 8.1 +# * 8.2 +# * 8.2.14 +# * latest +# +# Do *NOT* use the full Docker tag (e.g. "8.3.2RC1-fpm-bullseye") +# *only* the version part. The rest of the full tag is derived from +# the [DOCKER_APP_RUNTIME] and [PHP_DEBIAN_RELEASE] settings +# @dottie/validate required +DOCKER_APP_PHP_VERSION="8.2" -## EXPERIMENTS -EXP_LC=false -EXP_REC=false -EXP_LOOPS=false +# The container runtime to use. +# +# @see https://docs.pixelfed.org/running-pixelfed/docker/runtimes.html +# @dottie/validate required,oneof=apache nginx fpm +DOCKER_APP_RUNTIME="apache" -## ActivityPub Federation -ACTIVITY_PUB=false -AP_REMOTE_FOLLOW=false -AP_SHAREDINBOX=false -AP_INBOX=false -AP_OUTBOX=false -ATOM_FEEDS=true -NODEINFO=true -WEBFINGER=true +# The Debian release variant to use of the [php] Docker image +# +# Examlpe: [bookworm] or [bullseye] +# @dottie/validate required,oneof=bookworm bullseye +DOCKER_APP_DEBIAN_RELEASE="bullseye" -## S3 -FILESYSTEM_CLOUD=s3 -PF_ENABLE_CLOUD=false -#AWS_ACCESS_KEY_ID= -#AWS_SECRET_ACCESS_KEY= -#AWS_DEFAULT_REGION= -#AWS_BUCKET= -#AWS_URL= -#AWS_ENDPOINT= -#AWS_USE_PATH_STYLE_ENDPOINT=false +# The [php] Docker image base type +# +# @see https://docs.pixelfed.org/running-pixelfed/docker/runtimes.html +# @dottie/validate required,oneof=apache fpm cli +DOCKER_APP_BASE_TYPE="apache" -## Horizon -HORIZON_DARKMODE=false +# Image to pull the Pixelfed Docker images from. +# +# Example values: +# +# * "ghcr.io/pixelfed/pixelfed" to pull from GitHub +# * "pixelfed/pixelfed" to pull from DockerHub +# * "your/fork" to pull from a custom fork +# +# @dottie/validate required +DOCKER_APP_IMAGE="ghcr.io/jippi/pixelfed" -## COSTAR - Confirm Object Sentiment Transform and Reduce -PF_COSTAR_ENABLED=false +# Pixelfed version (image tag) to pull from the registry. +# +# @see https://github.com/pixelfed/pixelfed/pkgs/container/pixelfed +# @dottie/validate required +DOCKER_APP_TAG="${DOCKER_APP_RELEASE:?error}-${DOCKER_APP_RUNTIME:?error}-${DOCKER_APP_PHP_VERSION:?error}" -# Media -MEDIA_EXIF_DATABASE=false +# Path (on host system) where the [app] + [worker] container will write +# its [storage] data (e.g uploads/images/profile pictures etc.). +# +# Path is relative (./some/other/path) to the docker-compose.yml or absolute (/some/other/path) +# @dottie/validate required,dir +DOCKER_APP_HOST_STORAGE_PATH="${DOCKER_ALL_HOST_DATA_ROOT_PATH:?error}/pixelfed/storage" -## Logging -LOG_CHANNEL=stderr +# Path (on host system) where the [app] + [worker] container will write +# its [cache] data. +# +# Path is relative (./some/other/path) to the docker-compose.yml or absolute (/some/other/path) +# @dottie/validate required,dir +DOCKER_APP_HOST_CACHE_PATH="${DOCKER_ALL_HOST_DATA_ROOT_PATH:?error}/pixelfed/cache" -## Image -IMAGE_DRIVER=imagick +# Automatically run "One-time setup tasks" commands. +# +# If you are migrating to this docker-compose setup or have manually run the "One time seutp" +# tasks (https://docs.pixelfed.org/running-pixelfed/installation/#setting-up-services) +# you can set this to "0" to prevent them from running. +# +# Otherwise, leave it at "1" to have them run *once*. +# @dottie/validate required,boolean +#DOCKER_APP_RUN_ONE_TIME_SETUP_TASKS="1" -## Broadcasting: log driver for local development -BROADCAST_DRIVER=log +# A space-seperated list of paths (inside the container) to *recursively* [chown] +# to the container user/group id (UID/GID) in case of permission issues. +# +# ! You should *not* leave this on permanently, at it can significantly slow down startup +# ! time for the container, and during normal operations there should never be permission +# ! issues. Please report a bug if you see behavior requiring this to be permanently on +# +# Example: "/var/www/storage /var/www/bootstrap/cache" +# @dottie/validate required +#DOCKER_APP_ENSURE_OWNERSHIP_PATHS="" -## Cache -CACHE_DRIVER=redis +# Enable Docker Entrypoint debug mode (will call [set -x] in bash scripts) +# by setting this to "1" +# @dottie/validate required,boolean +#DOCKER_APP_ENTRYPOINT_DEBUG="0" -## Purify -RESTRICT_HTML_TYPES=true +# Show the "diff" when applying templating to files +# +# @default "1" +# @dottie/validate required,boolean +#DOCKER_APP_ENTRYPOINT_SHOW_TEMPLATE_DIFF="1" -## Queue -QUEUE_DRIVER=redis +# Docker entrypoints that should be skipped on startup +# @default "" +#ENTRYPOINT_SKIP_SCRIPTS="" -## Session -SESSION_DRIVER=redis +# List of extra APT packages (separated by space) to install when building +# locally using [docker compose build]. +# +# @see https://github.com/pixelfed/pixelfed/blob/dev/docker/customizing.md +# @dottie/validate required +#DOCKER_APP_APT_PACKAGES_EXTRA="" -## Trusted Proxy -TRUST_PROXIES="*" +# List of *extra* PECL extensions (separated by space) to install when +# building locally using [docker compose build]. +# +# @see https://github.com/pixelfed/pixelfed/blob/dev/docker/customizing.md +# @dottie/validate required +#DOCKER_APP_PHP_PECL_EXTENSIONS_EXTRA="" -## Passport -#PASSPORT_PRIVATE_KEY= -#PASSPORT_PUBLIC_KEY= +# List of *extra* PHP extensions (separated by space) to install when +# building locally using [docker compose build]. +# +# @see https://github.com/pixelfed/pixelfed/blob/dev/docker/customizing.md +# @dottie/validate required +#DOCKER_APP_PHP_EXTENSIONS_EXTRA="" + +# @default "128M" +# @see https://www.php.net/manual/en/ini.core.php#ini.memory-limit +# @dottie/validate required +#DOCKER_APP_PHP_MEMORY_LIMIT="128M" + +# @default "E_ALL & ~E_DEPRECATED & ~E_STRICT" +# @see http://php.net/error-reporting +# @dottie/validate required +#DOCKER_APP_PHP_ERROR_REPORTING="E_ALL & ~E_DEPRECATED & ~E_STRICT" + +# @default "off" +# @see http://php.net/display-errors +# @dottie/validate required,oneof=on off +#DOCKER_APP_PHP_DISPLAY_ERRORS="off" + +# Enables the opcode cache. +# +# When disabled, code is not optimised or cached. +# +# @default "1" +# @see https://www.php.net/manual/en/opcache.configuration.php#ini.opcache.enable +# @dottie/validate required,oneof=0 1 +#DOCKER_APP_PHP_OPCACHE_ENABLE="1" + +# If enabled, OPcache will check for updated scripts every [opcache.revalidate_freq] seconds. +# +# When this directive is disabled, you must reset OPcache manually via opcache_reset(), +# opcache_invalidate() or by restarting the Web server for changes to the filesystem to take effect. +# +# @default "0" +# @see https://www.php.net/manual/en/opcache.configuration.php#ini.opcache.validate-timestamps +# @dottie/validate required,oneof=0 1 +#DOCKER_APP_PHP_OPCACHE_VALIDATE_TIMESTAMPS="0" + +# How often to check script timestamps for updates, in seconds. +# 0 will result in OPcache checking for updates on every request. +# +# @default "2" +# @see https://www.php.net/manual/en/opcache.configuration.php#ini.opcache.revalidate-freq +# @dottie/validate required,oneof=0 1 2 +#DOCKER_APP_PHP_OPCACHE_REVALIDATE_FREQ="2" + +################################################################################ +# docker redis +################################################################################ + +# Set this to a non-empty value (e.g. "disabled") to disable the [redis] service +#DOCKER_REDIS_PROFILE= + +# Redis version to use as Docker tag +# +# @see https://hub.docker.com/_/redis +# @dottie/validate required +DOCKER_REDIS_VERSION="7.2" + +# Path (on host system) where the [redis] container will store its data +# +# Path is relative (./some/other/path) to the docker-compose.yml or absolute (/some/other/path) +# @dottie/validate required,dir +DOCKER_REDIS_HOST_DATA_PATH="${DOCKER_ALL_HOST_DATA_ROOT_PATH:?error}/redis" + +# Port that Redis will listen on *outside* the container (e.g. the host machine) +# @dottie/validate required,number +DOCKER_REDIS_HOST_PORT="${REDIS_PORT:?error}" + +# The filename that Redis should store its config file within +# +# NOTE: The file *MUST* exists (even empty) before enabling this setting! +# +# Use a command like [touch "${DOCKER_ALL_HOST_CONFIG_ROOT_PATH}/redis/redis.conf"] to create it. +# +# @default "" +# @dottie/validate required +#DOCKER_REDIS_CONFIG_FILE="/etc/redis/redis.conf" +# How often Docker health check should run for [redis] service +# +# @default "10s" +# @dottie/validate required +DOCKER_REDIS_HEALTHCHECK_INTERVAL="${DOCKER_ALL_DEFAULT_HEALTHCHECK_INTERVAL:?error}" + +################################################################################ +# docker db +################################################################################ + +# Set this to a non-empty value (e.g. "disabled") to disable the [db] service +#DOCKER_DB_PROFILE= + +# Docker image for the DB service +# @dottie/validate required +DOCKER_DB_IMAGE="mariadb:${DB_VERSION}" + +# Command to pass to the [db] server container +# @dottie/validate required +DOCKER_DB_COMMAND="--default-authentication-plugin=mysql_native_password" + +# Path (on host system) where the [db] container will store its data +# +# Path is relative (./some/other/path) to the docker-compose.yml or absolute (/some/other/path) +# @dottie/validate required,dir +DOCKER_DB_HOST_DATA_PATH="${DOCKER_ALL_HOST_DATA_ROOT_PATH:?error}/db" + +# Path (inside the container) where the [db] will store its data. +# +# Path MUST be absolute. +# +# For MySQL this should be [/var/lib/mysql] +# For PostgreSQL this should be [/var/lib/postgresql/data] +# @dottie/validate required +DOCKER_DB_CONTAINER_DATA_PATH="/var/lib/mysql" + +# Port that the database will listen on *OUTSIDE* the container (e.g. the host machine) +# +# Use "3306" for MySQL/MariaDB and "5432" for PostgreeSQL +# @dottie/validate required,number +DOCKER_DB_HOST_PORT="${DB_PORT:?error}" + +# Port that the database will listen on *INSIDE* the container +# +# Use "3306" for MySQL/MariaDB and "5432" for PostgreeSQL +# @dottie/validate required,number +DOCKER_DB_CONTAINER_PORT="${DB_PORT:?error}" + +# How often Docker health check should run for [db] service +# @dottie/validate required +DOCKER_DB_HEALTHCHECK_INTERVAL="${DOCKER_ALL_DEFAULT_HEALTHCHECK_INTERVAL:?error}" + +################################################################################ +# docker web +################################################################################ + +# Set this to a non-empty value (e.g. "disabled") to disable the [web] service +#DOCKER_WEB_PROFILE="" + +# Port to expose [web] container will listen on *outside* the container (e.g. the host machine) for *HTTP* traffic only +# @dottie/validate required,number +DOCKER_WEB_PORT_EXTERNAL_HTTP="8080" + +# How often Docker health check should run for [web] service +# @dottie/validate required +DOCKER_WEB_HEALTHCHECK_INTERVAL="${DOCKER_ALL_DEFAULT_HEALTHCHECK_INTERVAL:?error}" + +################################################################################ +# docker worker +################################################################################ + +# Set this to a non-empty value (e.g. "disabled") to disable the [worker] service +#DOCKER_WORKER_PROFILE="" + +# How often Docker health check should run for [worker] service +# @dottie/validate required +DOCKER_WORKER_HEALTHCHECK_INTERVAL="${DOCKER_ALL_DEFAULT_HEALTHCHECK_INTERVAL:?error}" + +################################################################################ +# docker proxy +################################################################################ + +# Set this to a non-empty value (e.g. "disabled") to disable the [proxy] and [proxy-acme] service +#DOCKER_PROXY_PROFILE= + +# Set this to a non-empty value (e.g. "disabled") to disable the [proxy-acme] service +#DOCKER_PROXY_ACME_PROFILE="${DOCKER_PROXY_PROFILE:-}" + +# The version of nginx-proxy to use +# +# @see https://hub.docker.com/r/nginxproxy/nginx-proxy +# @dottie/validate required +DOCKER_PROXY_VERSION="1.4" + +# How often Docker health check should run for [proxy] service +# @dottie/validate required +DOCKER_PROXY_HEALTHCHECK_INTERVAL="${DOCKER_ALL_DEFAULT_HEALTHCHECK_INTERVAL:?error}" + +# Port that the [proxy] will listen on *outside* the container (e.g. the host machine) for HTTP traffic +# @dottie/validate required,number +DOCKER_PROXY_HOST_PORT_HTTP="80" + +# Port that the [proxy] will listen on *outside* the container (e.g. the host machine) for HTTPS traffic +# @dottie/validate required,number +DOCKER_PROXY_HOST_PORT_HTTPS="443" + +# Path to the Docker socket on the *host* +# @dottie/validate required,file +DOCKER_PROXY_HOST_DOCKER_SOCKET_PATH="/var/run/docker.sock" + +# The host to request LetsEncrypt certificate for +# @dottie/validate required,fqdn +DOCKER_PROXY_LETSENCRYPT_HOST="${APP_DOMAIN}" + +# The e-mail to use for Lets Encrypt certificate requests. +# @dottie/validate required,email +DOCKER_PROXY_LETSENCRYPT_EMAIL="${INSTANCE_CONTACT_EMAIL:?error}" + +# Lets Encrypt staging/test servers for certificate requests. +# +# Setting this to any value will change to letsencrypt test servers. +#DOCKER_PROXY_LETSENCRYPT_TEST="1" diff --git a/.env.example b/.env.example deleted file mode 100644 index d4d7228d1..000000000 --- a/.env.example +++ /dev/null @@ -1,78 +0,0 @@ -APP_NAME="Pixelfed" -APP_ENV="production" -APP_KEY= -APP_DEBUG="false" - -# Instance Configuration -OPEN_REGISTRATION="false" -ENFORCE_EMAIL_VERIFICATION="false" -PF_MAX_USERS="1000" -OAUTH_ENABLED="true" - -# Media Configuration -PF_OPTIMIZE_IMAGES="true" -IMAGE_QUALITY="80" -MAX_PHOTO_SIZE="15000" -MAX_CAPTION_LENGTH="500" -MAX_ALBUM_LENGTH="4" - -# Instance URL Configuration -APP_URL="http://localhost" -APP_DOMAIN="localhost" -ADMIN_DOMAIN="localhost" -SESSION_DOMAIN="localhost" -TRUST_PROXIES="*" - -# Database Configuration -DB_CONNECTION="mysql" -DB_HOST="127.0.0.1" -DB_PORT="3306" -DB_DATABASE="pixelfed" -DB_USERNAME="pixelfed" -DB_PASSWORD="pixelfed" - -# Redis Configuration -REDIS_CLIENT="predis" -REDIS_SCHEME="tcp" -REDIS_HOST="127.0.0.1" -REDIS_PASSWORD="null" -REDIS_PORT="6379" - -# Laravel Configuration -SESSION_DRIVER="database" -CACHE_DRIVER="redis" -QUEUE_DRIVER="redis" -BROADCAST_DRIVER="log" -LOG_CHANNEL="stack" -HORIZON_PREFIX="horizon-" - -# ActivityPub Configuration -ACTIVITY_PUB="false" -AP_REMOTE_FOLLOW="false" -AP_INBOX="false" -AP_OUTBOX="false" -AP_SHAREDINBOX="false" - -# Experimental Configuration -EXP_EMC="true" - -## Mail Configuration (Post-Installer) -MAIL_DRIVER=log -MAIL_HOST=smtp.mailtrap.io -MAIL_PORT=2525 -MAIL_USERNAME=null -MAIL_PASSWORD=null -MAIL_ENCRYPTION=null -MAIL_FROM_ADDRESS="pixelfed@example.com" -MAIL_FROM_NAME="Pixelfed" - -## S3 Configuration (Post-Installer) -PF_ENABLE_CLOUD=false -FILESYSTEM_CLOUD=s3 -#AWS_ACCESS_KEY_ID= -#AWS_SECRET_ACCESS_KEY= -#AWS_DEFAULT_REGION= -#AWS_BUCKET= -#AWS_URL= -#AWS_ENDPOINT= -#AWS_USE_PATH_STYLE_ENDPOINT=false diff --git a/.env.testing b/.env.testing index 258d8d740..63209d91b 100644 --- a/.env.testing +++ b/.env.testing @@ -1,3 +1,5 @@ +# shellcheck disable=SC2034,SC2148 + APP_NAME="Pixelfed Test" APP_ENV=local APP_KEY=base64:lwX95GbNWX3XsucdMe0XwtOKECta3h/B+p9NbH2jd0E= @@ -62,8 +64,8 @@ CS_BLOCKED_DOMAINS='example.org,example.net,example.com' CS_CW_DOMAINS='example.org,example.net,example.com' CS_UNLISTED_DOMAINS='example.org,example.net,example.com' -## Optional +## Optional #HORIZON_DARKMODE=false # Horizon theme darkmode -#HORIZON_EMBED=false # Single Docker Container mode +#HORIZON_EMBED=false # Single Docker Container mode ENABLE_CONFIG_CACHE=false diff --git a/.github/workflows/build-docker.yml b/.github/workflows/build-docker.yml deleted file mode 100644 index 34f31cf08..000000000 --- a/.github/workflows/build-docker.yml +++ /dev/null @@ -1,125 +0,0 @@ ---- -name: Build Docker image - -on: - workflow_dispatch: - push: - branches: - - dev - tags: - - '*' - pull_request: - paths: - - .github/workflows/build-docker.yml - - contrib/docker/Dockerfile.apache - - contrib/docker/Dockerfile.fpm -permissions: - contents: read - -jobs: - build-docker-apache: - runs-on: ubuntu-latest - - steps: - - name: Checkout Code - uses: actions/checkout@v3 - - - name: Docker Lint - uses: hadolint/hadolint-action@v3.0.0 - with: - dockerfile: contrib/docker/Dockerfile.apache - failure-threshold: error - - - name: Set up QEMU - uses: docker/setup-qemu-action@v2 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - - - name: Login to DockerHub - uses: docker/login-action@v2 - secrets: inherit - with: - username: ${{ secrets.DOCKER_HUB_USERNAME }} - password: ${{ secrets.DOCKER_HUB_TOKEN }} - if: github.event_name != 'pull_request' - - - name: Fetch tags - uses: docker/metadata-action@v4 - secrets: inherit - id: meta - with: - images: ${{ secrets.DOCKER_HUB_ORGANISATION }}/pixelfed - flavor: | - latest=auto - suffix=-apache - tags: | - type=edge,branch=dev - type=pep440,pattern={{raw}} - type=pep440,pattern=v{{major}}.{{minor}} - type=ref,event=pr - - - name: Build and push Docker image - uses: docker/build-push-action@v3 - with: - context: . - file: contrib/docker/Dockerfile.apache - platforms: linux/amd64,linux/arm64 - builder: ${{ steps.buildx.outputs.name }} - push: ${{ github.event_name != 'pull_request' }} - tags: ${{ steps.meta.outputs.tags }} - cache-from: type=gha - cache-to: type=gha,mode=max - - build-docker-fpm: - runs-on: ubuntu-latest - - steps: - - name: Checkout Code - uses: actions/checkout@v3 - - - name: Docker Lint - uses: hadolint/hadolint-action@v3.0.0 - with: - dockerfile: contrib/docker/Dockerfile.fpm - failure-threshold: error - - - name: Set up QEMU - uses: docker/setup-qemu-action@v2 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - - - name: Login to DockerHub - uses: docker/login-action@v2 - secrets: inherit - with: - username: ${{ secrets.DOCKER_HUB_USERNAME }} - password: ${{ secrets.DOCKER_HUB_TOKEN }} - if: github.event_name != 'pull_request' - - - name: Fetch tags - uses: docker/metadata-action@v4 - secrets: inherit - id: meta - with: - images: ${{ secrets.DOCKER_HUB_ORGANISATION }}/pixelfed - flavor: | - suffix=-fpm - tags: | - type=edge,branch=dev - type=pep440,pattern={{raw}} - type=pep440,pattern=v{{major}}.{{minor}} - type=ref,event=pr - - - name: Build and push Docker image - uses: docker/build-push-action@v3 - with: - context: . - file: contrib/docker/Dockerfile.fpm - platforms: linux/amd64,linux/arm64 - builder: ${{ steps.buildx.outputs.name }} - push: ${{ github.event_name != 'pull_request' }} - tags: ${{ steps.meta.outputs.tags }} - cache-from: type=gha - cache-to: type=gha,mode=max diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 000000000..d14c88dfc --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,231 @@ +--- +name: Docker + +on: + # See: https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#workflow_dispatch + workflow_dispatch: + + # See: https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#push + push: + branches: + - dev + - staging + - jippi-fork # TODO(jippi): remove me before merge + tags: + - "*" + + # See: https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request + pull_request: + types: + - opened + - reopened + - synchronize + +jobs: + lint: + name: hadolint + runs-on: ubuntu-latest + + permissions: + contents: read + + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Docker Lint + uses: hadolint/hadolint-action@v3.1.0 + with: + dockerfile: Dockerfile + failure-threshold: error + + shellcheck: + name: ShellCheck + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Run ShellCheck + uses: ludeeus/action-shellcheck@master + env: + SHELLCHECK_OPTS: --shell=bash --external-sources + with: + version: v0.9.0 + additional_files: "*.envsh .env .env.docker .env.example .env.testing" + + bats: + name: Bats Testing + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Run bats + run: docker run -v "$PWD:/var/www" bats/bats:latest /var/www/tests/bats + + build: + name: Build, Test, and Push + runs-on: ubuntu-latest + + strategy: + fail-fast: false + + # See: https://docs.github.com/en/actions/using-jobs/using-a-matrix-for-your-jobs + matrix: + php_version: + - 8.2 + - 8.3 + target_runtime: + - apache + - fpm + - nginx + php_base: + - apache + - fpm + + # See: https://docs.github.com/en/actions/using-jobs/using-a-matrix-for-your-jobs#excluding-matrix-configurations + # See: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstrategymatrixexclude + exclude: + # targeting [apache] runtime with [fpm] base type doesn't make sense + - target_runtime: apache + php_base: fpm + + # targeting [fpm] runtime with [apache] base type doesn't make sense + - target_runtime: fpm + php_base: apache + + # targeting [nginx] runtime with [apache] base type doesn't make sense + - target_runtime: nginx + php_base: apache + + # See: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#example-using-concurrency-and-the-default-behavior + concurrency: + group: docker-build-${{ github.ref }}-${{ matrix.php_base }}-${{ matrix.php_version }}-${{ matrix.target_runtime }} + cancel-in-progress: true + + permissions: + contents: read + packages: write + + env: + # Set the repo variable [DOCKER_HUB_USERNAME] to override the default + # at https://github.com///settings/variables/actions + DOCKER_HUB_USERNAME: ${{ vars.DOCKER_HUB_USERNAME || 'pixelfed' }} + + # Set the repo variable [DOCKER_HUB_ORGANISATION] to override the default + # at https://github.com///settings/variables/actions + DOCKER_HUB_ORGANISATION: ${{ vars.DOCKER_HUB_ORGANISATION || 'pixelfed' }} + + # Set the repo variable [DOCKER_HUB_REPO] to override the default + # at https://github.com///settings/variables/actions + DOCKER_HUB_REPO: ${{ vars.DOCKER_HUB_REPO || 'pixelfed' }} + + # For Docker Hub pushing to work, you need the secret [DOCKER_HUB_TOKEN] + # set to your Personal Access Token at https://github.com///settings/secrets/actions + # + # ! NOTE: no [login] or [push] will happen to Docker Hub until this secret is set! + HAS_DOCKER_HUB_CONFIGURED: ${{ secrets.DOCKER_HUB_TOKEN != '' }} + + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + id: buildx + with: + version: v0.12.0 # *or* newer, needed for annotations to work + + # See: https://github.com/docker/login-action?tab=readme-ov-file#github-container-registry + - name: Log in to the GitHub Container registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # See: https://github.com/docker/login-action?tab=readme-ov-file#docker-hub + - name: Login to Docker Hub registry (conditionally) + if: ${{ env.HAS_DOCKER_HUB_CONFIGURED == true }} + uses: docker/login-action@v3 + with: + username: ${{ env.DOCKER_HUB_USERNAME }} + password: ${{ secrets.DOCKER_HUB_TOKEN }} + + - name: Docker meta + uses: docker/metadata-action@v5 + id: meta + with: + images: | + name=ghcr.io/${{ github.repository }},enable=true + name=${{ env.DOCKER_HUB_ORGANISATION }}/${{ env.DOCKER_HUB_REPO }},enable=${{ env.HAS_DOCKER_HUB_CONFIGURED }} + flavor: | + latest=auto + suffix=-${{ matrix.target_runtime }}-${{ matrix.php_version }} + tags: | + type=raw,value=dev,enable=${{ github.ref == format('refs/heads/{0}', 'dev') }} + type=raw,value=staging,enable=${{ github.ref == format('refs/heads/{0}', 'staging') }} + type=pep440,pattern={{raw}} + type=pep440,pattern=v{{major}}.{{minor}} + type=ref,event=branch,prefix=branch- + type=ref,event=pr,prefix=pr- + type=ref,event=tag + env: + DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index + + - name: Docker meta (Cache) + uses: docker/metadata-action@v5 + id: cache + with: + images: | + name=ghcr.io/${{ github.repository }}-cache,enable=true + name=${{ env.DOCKER_HUB_ORGANISATION }}/${{ env.DOCKER_HUB_REPO }}-cache,enable=${{ env.HAS_DOCKER_HUB_CONFIGURED }} + flavor: | + latest=auto + suffix=-${{ matrix.target_runtime }}-${{ matrix.php_version }} + tags: | + type=raw,value=dev,enable=${{ github.ref == format('refs/heads/{0}', 'dev') }} + type=raw,value=staging,enable=${{ github.ref == format('refs/heads/{0}', 'staging') }} + type=pep440,pattern={{raw}} + type=pep440,pattern=v{{major}}.{{minor}} + type=ref,event=branch,prefix=branch- + type=ref,event=pr,prefix=pr- + type=ref,event=tag + env: + DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + file: Dockerfile + target: ${{ matrix.target_runtime }}-runtime + platforms: linux/amd64,linux/arm64 + builder: ${{ steps.buildx.outputs.name }} + tags: ${{ steps.meta.outputs.tags }} + annotations: ${{ steps.meta.outputs.annotations }} + push: true + sbom: true + provenance: true + build-args: | + PHP_VERSION=${{ matrix.php_version }} + PHP_BASE_TYPE=${{ matrix.php_base }} + cache-from: | + type=gha,scope=${{ matrix.target_runtime }}-${{ matrix.php_base }}-${{ matrix.php_version }} + cache-to: | + type=gha,mode=max,scope=${{ matrix.target_runtime }}-${{ matrix.php_base }}-${{ matrix.php_version }} + ${{ steps.cache.outputs.tags }} + + # goss validate the image + # + # See: https://github.com/goss-org/goss + - uses: e1himself/goss-installation-action@v1 + with: + version: "v0.4.4" + - name: Execute Goss tests + run: | + dgoss run \ + -v "./.env.testing:/var/www/.env" \ + -e "EXPECTED_PHP_VERSION=${{ matrix.php_version }}" \ + -e "PHP_BASE_TYPE=${{ matrix.php_base }}" \ + ${{ steps.meta.outputs.tags }} diff --git a/.gitignore b/.gitignore index 0494cee10..ba2c7d07f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,22 +1,30 @@ +.DS_Store +/.bash_history +/.bash_profile +/.bashrc +/.composer +/.env +/.env.dottie-backup +#/.git +/.git-credentials +/.gitconfig +#/.gitignore +/.idea +/.vagrant +/bootstrap/cache +/docker-compose-state/ +/Homestead.json +/Homestead.yaml /node_modules +/npm-debug.log /public/hot /public/storage +/public/vendor/horizon /storage/*.key +/storage/docker /vendor -/.idea -/.vscode -/.vagrant -/docker-volumes -Homestead.json -Homestead.yaml -npm-debug.log -yarn-error.log -.env -.DS_Store -.bash_profile -.bash_history -.bashrc -.gitconfig -.git-credentials -/.composer/ -/nginx.conf +/yarn-error.log + +# Exceptions - these *MUST* be last +!/bootstrap/cache/.gitignore +!/public/vendor/horizon/.gitignore diff --git a/.hadolint.yaml b/.hadolint.yaml new file mode 100644 index 000000000..cbb62ca47 --- /dev/null +++ b/.hadolint.yaml @@ -0,0 +1,5 @@ +ignored: + - DL3002 # warning: Last USER should not be root + - DL3008 # warning: Pin versions in apt get install. Instead of `apt-get install ` use `apt-get install =` + - SC2046 # warning: Quote this to prevent word splitting. + - SC2086 # info: Double quote to prevent globbing and word splitting. diff --git a/.markdownlint.json b/.markdownlint.json new file mode 100644 index 000000000..cf98a0902 --- /dev/null +++ b/.markdownlint.json @@ -0,0 +1,4 @@ +{ + "MD013": false, + "MD014": false +} diff --git a/.shellcheckrc b/.shellcheckrc new file mode 100644 index 000000000..be92f81fc --- /dev/null +++ b/.shellcheckrc @@ -0,0 +1,12 @@ +# See: https://github.com/koalaman/shellcheck/blob/master/shellcheck.1.md#rc-files + +source-path=SCRIPTDIR + +# Allow opening any 'source'd file, even if not specified as input +external-sources=true + +# Turn on warnings for unquoted variables with safe values +enable=quote-safe-variables + +# Turn on warnings for unassigned uppercase variables +enable=check-unassigned-uppercase diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 000000000..128e0a295 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,14 @@ +{ + "recommendations": [ + "foxundermoon.shell-format", + "timonwong.shellcheck", + "jetmartin.bats", + "aaron-bond.better-comments", + "streetsidesoftware.code-spell-checker", + "editorconfig.editorconfig", + "github.vscode-github-actions", + "bmewburn.vscode-intelephense-client", + "redhat.vscode-yaml", + "ms-azuretools.vscode-docker" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..6446fb6f5 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,21 @@ +{ + "shellformat.useEditorConfig": true, + "[shellscript]": { + "files.eol": "\n", + "editor.defaultFormatter": "foxundermoon.shell-format" + }, + "[yaml]": { + "editor.defaultFormatter": "redhat.vscode-yaml" + }, + "[dockercompose]": { + "editor.defaultFormatter": "redhat.vscode-yaml", + "editor.autoIndent": "advanced", + }, + "yaml.schemas": { + "https://json.schemastore.org/composer": "https://raw.githubusercontent.com/compose-spec/compose-spec/master/schema/compose-spec.json" + }, + "files.associations": { + ".env": "shellscript", + ".env.*": "shellscript" + } +} diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 000000000..69a06cf87 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,18 @@ +# See: https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners + +# These owners will be the default owners for everything in +# the repo. Unless a later match takes precedence, +* @dansup + +# Docker related files +.editorconfig @jippi @dansup +.env @jippi @dansup +.env.* @jippi @dansup +.hadolint.yaml @jippi @dansup +.shellcheckrc @jippi @dansup +/.github/ @jippi @dansup +/docker/ @jippi @dansup +/tests/ @jippi @dansup +docker-compose.migrate.yml @jippi @dansup +docker-compose.yml @jippi @dansup +goss.yaml @jippi @dansup diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..a6ad884b8 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,307 @@ +# syntax=docker/dockerfile:1 +# See https://hub.docker.com/r/docker/dockerfile + +####################################################### +# Configuration +####################################################### + +# See: https://github.com/mlocati/docker-php-extension-installer +ARG DOCKER_PHP_EXTENSION_INSTALLER_VERSION="2.1.80" + +# See: https://github.com/composer/composer +ARG COMPOSER_VERSION="2.6" + +# See: https://nginx.org/ +ARG NGINX_VERSION="1.25.3" + +# See: https://github.com/ddollar/forego +ARG FOREGO_VERSION="0.17.2" + +# See: https://github.com/hairyhenderson/gomplate +ARG GOMPLATE_VERSION="v3.11.6" + +# See: https://github.com/jippi/dottie +ARG DOTTIE_VERSION="v0.9.5" + +### +# PHP base configuration +### + +# See: https://hub.docker.com/_/php/tags +ARG PHP_VERSION="8.1" + +# See: https://github.com/docker-library/docs/blob/master/php/README.md#image-variants +ARG PHP_BASE_TYPE="apache" +ARG PHP_DEBIAN_RELEASE="bullseye" + +ARG RUNTIME_UID=33 # often called 'www-data' +ARG RUNTIME_GID=33 # often called 'www-data' + +# APT extra packages +ARG APT_PACKAGES_EXTRA= + +# Extensions installed via [pecl install] +# ! NOTE: imagick is installed from [master] branch on GitHub due to 8.3 bug on ARM that haven't +# ! been released yet (after +10 months)! +# ! See: https://github.com/Imagick/imagick/pull/641 +ARG PHP_PECL_EXTENSIONS="redis https://codeload.github.com/Imagick/imagick/tar.gz/28f27044e435a2b203e32675e942eb8de620ee58" +ARG PHP_PECL_EXTENSIONS_EXTRA= + +# Extensions installed via [docker-php-ext-install] +ARG PHP_EXTENSIONS="intl bcmath zip pcntl exif curl gd" +ARG PHP_EXTENSIONS_EXTRA="" +ARG PHP_EXTENSIONS_DATABASE="pdo_pgsql pdo_mysql pdo_sqlite" + +# GPG key for nginx apt repository +ARG NGINX_GPGKEY="573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62" + +# GPP key path for nginx apt repository +ARG NGINX_GPGKEY_PATH="/usr/share/keyrings/nginx-archive-keyring.gpg" + +####################################################### +# Docker "copy from" images +####################################################### + +# Composer docker image from Docker Hub +# +# NOTE: Docker will *not* pull this image unless it's referenced (via build target) +FROM composer:${COMPOSER_VERSION} AS composer-image + +# php-extension-installer image from Docker Hub +# +# NOTE: Docker will *not* pull this image unless it's referenced (via build target) +FROM mlocati/php-extension-installer:${DOCKER_PHP_EXTENSION_INSTALLER_VERSION} AS php-extension-installer + +# nginx webserver from Docker Hub. +# Used to copy some docker-entrypoint files for [nginx-runtime] +# +# NOTE: Docker will *not* pull this image unless it's referenced (via build target) +FROM nginx:${NGINX_VERSION} AS nginx-image + +# Forego is a Procfile "runner" that makes it trival to run multiple +# processes under a simple init / PID 1 process. +# +# NOTE: Docker will *not* pull this image unless it's referenced (via build target) +# +# See: https://github.com/nginx-proxy/forego +FROM nginxproxy/forego:${FOREGO_VERSION}-debian AS forego-image + +# Dottie makes working with .env files easier and safer +# +# NOTE: Docker will *not* pull this image unless it's referenced (via build target) +# +# See: https://github.com/jippi/dottie +FROM ghcr.io/jippi/dottie:${DOTTIE_VERSION} AS dottie-image + +# gomplate-image grabs the gomplate binary from GitHub releases +# +# It's in its own layer so it can be fetched in parallel with other build steps +FROM php:${PHP_VERSION}-${PHP_BASE_TYPE}-${PHP_DEBIAN_RELEASE} AS gomplate-image + +ARG TARGETARCH +ARG TARGETOS +ARG GOMPLATE_VERSION + +RUN set -ex \ + && curl \ + --silent \ + --show-error \ + --location \ + --output /usr/local/bin/gomplate \ + https://github.com/hairyhenderson/gomplate/releases/download/${GOMPLATE_VERSION}/gomplate_${TARGETOS}-${TARGETARCH} \ + && chmod +x /usr/local/bin/gomplate \ + && /usr/local/bin/gomplate --version + +####################################################### +# Base image +####################################################### + +FROM php:${PHP_VERSION}-${PHP_BASE_TYPE}-${PHP_DEBIAN_RELEASE} AS base + +ARG BUILDKIT_SBOM_SCAN_STAGE="true" + +ARG APT_PACKAGES_EXTRA +ARG PHP_DEBIAN_RELEASE +ARG PHP_VERSION +ARG RUNTIME_GID +ARG RUNTIME_UID +ARG TARGETPLATFORM + +ENV DEBIAN_FRONTEND="noninteractive" + +# Ensure we run all scripts through 'bash' rather than 'sh' +SHELL ["/bin/bash", "-c"] + +RUN set -ex \ + && mkdir -pv /var/www/ \ + && chown -R ${RUNTIME_UID}:${RUNTIME_GID} /var/www + +WORKDIR /var/www/ + +ENV APT_PACKAGES_EXTRA=${APT_PACKAGES_EXTRA} + +# Install and configure base layer +COPY docker/shared/root/docker/install/base.sh /docker/install/base.sh + +RUN --mount=type=cache,id=pixelfed-apt-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/var/lib/apt \ + --mount=type=cache,id=pixelfed-apt-cache-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/var/cache/apt \ + /docker/install/base.sh + +####################################################### +# PHP: extensions +####################################################### + +FROM base AS php-extensions + +ARG PHP_DEBIAN_RELEASE +ARG PHP_EXTENSIONS +ARG PHP_EXTENSIONS_DATABASE +ARG PHP_EXTENSIONS_EXTRA +ARG PHP_PECL_EXTENSIONS +ARG PHP_PECL_EXTENSIONS_EXTRA +ARG PHP_VERSION +ARG TARGETPLATFORM + +COPY --from=php-extension-installer /usr/bin/install-php-extensions /usr/local/bin/ + +COPY docker/shared/root/docker/install/php-extensions.sh /docker/install/php-extensions.sh + +RUN --mount=type=cache,id=pixelfed-pear-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/tmp/pear \ + --mount=type=cache,id=pixelfed-apt-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/var/lib/apt \ + --mount=type=cache,id=pixelfed-apt-cache-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/var/cache/apt \ + PHP_EXTENSIONS=${PHP_EXTENSIONS} \ + PHP_EXTENSIONS_DATABASE=${PHP_EXTENSIONS_DATABASE} \ + PHP_EXTENSIONS_EXTRA=${PHP_EXTENSIONS_EXTRA} \ + PHP_PECL_EXTENSIONS=${PHP_PECL_EXTENSIONS} \ + PHP_PECL_EXTENSIONS_EXTRA=${PHP_PECL_EXTENSIONS_EXTRA} \ + /docker/install/php-extensions.sh + +####################################################### +# PHP: composer and source code +####################################################### + +FROM php-extensions AS composer-and-src + +ARG PHP_VERSION +ARG PHP_DEBIAN_RELEASE +ARG RUNTIME_UID +ARG RUNTIME_GID +ARG TARGETPLATFORM + +# Make sure composer cache is targeting our cache mount later +ENV COMPOSER_CACHE_DIR="/cache/composer" + +# Don't enforce any memory limits for composer +ENV COMPOSER_MEMORY_LIMIT=-1 + +# Disable interactvitity from composer +ENV COMPOSER_NO_INTERACTION=1 + +# Copy composer from https://hub.docker.com/_/composer +COPY --link --from=composer-image /usr/bin/composer /usr/bin/composer + +#! Changing user to runtime user +USER ${RUNTIME_UID}:${RUNTIME_GID} + +# Install composer dependencies +# NOTE: we skip the autoloader generation here since we don't have all files avaliable (yet) +RUN --mount=type=cache,id=pixelfed-composer-${PHP_VERSION},sharing=locked,target=/cache/composer \ + --mount=type=bind,source=composer.json,target=/var/www/composer.json \ + --mount=type=bind,source=composer.lock,target=/var/www/composer.lock \ + set -ex \ + && composer install --prefer-dist --no-autoloader --ignore-platform-reqs + +# Copy all other files over +COPY --chown=${RUNTIME_UID}:${RUNTIME_GID} . /var/www/ + +####################################################### +# Runtime: base +####################################################### + +FROM php-extensions AS shared-runtime + +ARG RUNTIME_GID +ARG RUNTIME_UID + +ENV RUNTIME_UID=${RUNTIME_UID} +ENV RUNTIME_GID=${RUNTIME_GID} + +COPY --link --from=forego-image /usr/local/bin/forego /usr/local/bin/forego +COPY --link --from=dottie-image /dottie /usr/local/bin/dottie +COPY --link --from=gomplate-image /usr/local/bin/gomplate /usr/local/bin/gomplate +COPY --link --from=composer-image /usr/bin/composer /usr/bin/composer +COPY --link --from=composer-and-src --chown=${RUNTIME_UID}:${RUNTIME_GID} /var/www /var/www + +#! Changing user to runtime user +USER ${RUNTIME_UID}:${RUNTIME_GID} + +# Generate optimized autoloader now that we have all files around +RUN set -ex \ + && composer dump-autoload --optimize + +USER root + +# for detail why storage is copied this way, pls refer to https://github.com/pixelfed/pixelfed/pull/2137#discussion_r434468862 +RUN set -ex \ + && cp --recursive --link --preserve=all storage storage.skel \ + && rm -rf html && ln -s public html + +COPY docker/shared/root / + +ENTRYPOINT ["/docker/entrypoint.sh"] + +####################################################### +# Runtime: apache +####################################################### + +FROM shared-runtime AS apache-runtime + +COPY docker/apache/root / + +RUN set -ex \ + && a2enmod rewrite remoteip proxy proxy_http \ + && a2enconf remoteip + +CMD ["apache2-foreground"] + +####################################################### +# Runtime: fpm +####################################################### + +FROM shared-runtime AS fpm-runtime + +COPY docker/fpm/root / + +CMD ["php-fpm"] + +####################################################### +# Runtime: nginx +####################################################### + +FROM shared-runtime AS nginx-runtime + +ARG NGINX_GPGKEY +ARG NGINX_GPGKEY_PATH +ARG NGINX_VERSION +ARG PHP_DEBIAN_RELEASE +ARG PHP_VERSION +ARG TARGETPLATFORM + +# Install nginx dependencies +RUN --mount=type=cache,id=pixelfed-apt-lists-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/var/lib/apt/lists \ + --mount=type=cache,id=pixelfed-apt-cache-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/var/cache/apt \ + set -ex \ + && gpg1 --keyserver "hkp://keyserver.ubuntu.com:80" --keyserver-options timeout=10 --recv-keys "${NGINX_GPGKEY}" \ + && gpg1 --export "$NGINX_GPGKEY" > "$NGINX_GPGKEY_PATH" \ + && echo "deb [signed-by=${NGINX_GPGKEY_PATH}] https://nginx.org/packages/mainline/debian/ ${PHP_DEBIAN_RELEASE} nginx" >> /etc/apt/sources.list.d/nginx.list \ + && apt-get update \ + && apt-get install -y --no-install-recommends nginx=${NGINX_VERSION}* + +# copy docker entrypoints from the *real* nginx image directly +COPY --link --from=nginx-image /docker-entrypoint.d /docker/entrypoint.d/ +COPY docker/nginx/root / +COPY docker/nginx/Procfile . + +STOPSIGNAL SIGQUIT + +CMD ["forego", "start", "-r"] diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index 7953ea783..dcee73ee1 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -25,31 +25,32 @@ class Kernel extends ConsoleKernel */ protected function schedule(Schedule $schedule) { - $schedule->command('media:optimize')->hourlyAt(40); - $schedule->command('media:gc')->hourlyAt(5); - $schedule->command('horizon:snapshot')->everyFiveMinutes(); - $schedule->command('story:gc')->everyFiveMinutes(); - $schedule->command('gc:failedjobs')->dailyAt(3); - $schedule->command('gc:passwordreset')->dailyAt('09:41'); - $schedule->command('gc:sessions')->twiceDaily(13, 23); + $schedule->command('media:optimize')->hourlyAt(40)->onOneServer(); + $schedule->command('media:gc')->hourlyAt(5)->onOneServer(); + $schedule->command('horizon:snapshot')->everyFiveMinutes()->onOneServer(); + $schedule->command('story:gc')->everyFiveMinutes()->onOneServer(); + $schedule->command('gc:failedjobs')->dailyAt(3)->onOneServer(); + $schedule->command('gc:passwordreset')->dailyAt('09:41')->onOneServer(); + $schedule->command('gc:sessions')->twiceDaily(13, 23)->onOneServer(); - if(in_array(config_cache('pixelfed.cloud_storage'), ['1', true, 'true']) && config('media.delete_local_after_cloud')) { + if (in_array(config_cache('pixelfed.cloud_storage'), ['1', true, 'true']) && config('media.delete_local_after_cloud')) { $schedule->command('media:s3gc')->hourlyAt(15); } - if(config('import.instagram.enabled')) { - $schedule->command('app:transform-imports')->everyTenMinutes(); - $schedule->command('app:import-upload-garbage-collection')->hourlyAt(51); - $schedule->command('app:import-remove-deleted-accounts')->hourlyAt(37); - $schedule->command('app:import-upload-clean-storage')->twiceDailyAt(1, 13, 32); + if (config('import.instagram.enabled')) { + $schedule->command('app:transform-imports')->everyTenMinutes()->onOneServer(); + $schedule->command('app:import-upload-garbage-collection')->hourlyAt(51)->onOneServer(); + $schedule->command('app:import-remove-deleted-accounts')->hourlyAt(37)->onOneServer(); + $schedule->command('app:import-upload-clean-storage')->twiceDailyAt(1, 13, 32)->onOneServer(); - if(config('import.instagram.storage.cloud.enabled') && (bool) config_cache('pixelfed.cloud_storage')) { - $schedule->command('app:import-upload-media-to-cloud-storage')->hourlyAt(39); + if (config('import.instagram.storage.cloud.enabled') && (bool) config_cache('pixelfed.cloud_storage')) { + $schedule->command('app:import-upload-media-to-cloud-storage')->hourlyAt(39)->onOneServer(); } } - $schedule->command('app:notification-epoch-update')->weeklyOn(1, '2:21'); - $schedule->command('app:hashtag-cached-count-update')->hourlyAt(25); - $schedule->command('app:account-post-count-stat-update')->everySixHours(25); + + $schedule->command('app:notification-epoch-update')->weeklyOn(1, '2:21')->onOneServer(); + $schedule->command('app:hashtag-cached-count-update')->hourlyAt(25)->onOneServer(); + $schedule->command('app:account-post-count-stat-update')->everySixHours(25)->onOneServer(); } /** @@ -59,7 +60,7 @@ class Kernel extends ConsoleKernel */ protected function commands() { - $this->load(__DIR__.'/Commands'); + $this->load(__DIR__ . '/Commands'); require base_path('routes/console.php'); } diff --git a/config/filesystems.php b/config/filesystems.php index 80e63ed99..00254e938 100644 --- a/config/filesystems.php +++ b/config/filesystems.php @@ -72,7 +72,7 @@ return [ 'secret' => env('AWS_SECRET_ACCESS_KEY'), 'region' => env('AWS_DEFAULT_REGION'), 'bucket' => env('AWS_BUCKET'), - 'visibility' => 'public', + 'visibility' => env('AWS_VISIBILITY', 'public'), 'url' => env('AWS_URL'), 'endpoint' => env('AWS_ENDPOINT'), 'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false), diff --git a/contrib/docker-nginx.conf b/contrib/docker-nginx.conf deleted file mode 100644 index 9d0a199e6..000000000 --- a/contrib/docker-nginx.conf +++ /dev/null @@ -1,35 +0,0 @@ -upstream fe { - server 127.0.0.1:8080; -} - -server { - server_name real.domain; - listen [::]:443 ssl ipv6only=on; - listen 443 ssl; - ssl_certificate /etc/letsencrypt/live/real.domain/fullchain.pem; # managed by Certbot - ssl_certificate_key /etc/letsencrypt/live/real.domain/privkey.pem; # managed by Certbot - include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot - ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot - - location / { - 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-Proto $scheme; - proxy_set_header X-Forwarded-Host $http_x_forwarded_host; - proxy_set_header X-Forwarded-Port $http_x_forwarded_port; - proxy_redirect off; - proxy_pass http://fe/; - } -} - -server { - if ($host = real.domain) { - return 301 https://$host$request_uri; - } - - listen 80; - listen [::]:80; - server_name real.domain; - return 404; -} \ No newline at end of file diff --git a/contrib/docker/Dockerfile.apache b/contrib/docker/Dockerfile.apache deleted file mode 100644 index a400f8797..000000000 --- a/contrib/docker/Dockerfile.apache +++ /dev/null @@ -1,100 +0,0 @@ -FROM php:8.1-apache-bullseye - -ENV COMPOSER_MEMORY_LIMIT=-1 -ARG DEBIAN_FRONTEND=noninteractive -WORKDIR /var/www/ - -# Get Composer binary -COPY --from=composer:2.4.4 /usr/bin/composer /usr/bin/composer - -# Install package dependencies -RUN apt-get update \ - && apt-get upgrade -y \ -# && apt-get install -y --no-install-recommends apt-utils \ - && apt-get install -y --no-install-recommends \ -## Standard - locales \ - locales-all \ - git \ - gosu \ - zip \ - unzip \ - libzip-dev \ - libcurl4-openssl-dev \ -## Image Optimization - optipng \ - pngquant \ - jpegoptim \ - gifsicle \ -## Image Processing - libjpeg62-turbo-dev \ - libpng-dev \ - libmagickwand-dev \ -# Required for GD - libxpm4 \ - libxpm-dev \ - libwebp7 \ - libwebp-dev \ -## Video Processing - ffmpeg \ -## Database -# libpq-dev \ -# libsqlite3-dev \ - mariadb-client \ -# Locales Update - && sed -i '/en_US/s/^#//g' /etc/locale.gen \ - && locale-gen \ - && update-locale \ -# Install PHP extensions - && docker-php-source extract \ -#PHP Imagemagick extensions - && pecl install imagick \ - && docker-php-ext-enable imagick \ -# PHP GD extensions - && docker-php-ext-configure gd \ - --with-freetype \ - --with-jpeg \ - --with-webp \ - --with-xpm \ - && docker-php-ext-install -j$(nproc) gd \ -#PHP Redis extensions - && pecl install redis \ - && docker-php-ext-enable redis \ -#PHP Database extensions - && docker-php-ext-install pdo_mysql \ -#pdo_pgsql pdo_sqlite \ -#PHP extensions (dependencies) - && docker-php-ext-configure intl \ - && docker-php-ext-install -j$(nproc) intl bcmath zip pcntl exif curl \ -#APACHE Bootstrap - && a2enmod rewrite remoteip \ - && {\ - echo RemoteIPHeader X-Real-IP ;\ - echo RemoteIPTrustedProxy 10.0.0.0/8 ;\ - echo RemoteIPTrustedProxy 172.16.0.0/12 ;\ - echo RemoteIPTrustedProxy 192.168.0.0/16 ;\ - echo SetEnvIf X-Forwarded-Proto "https" HTTPS=on ;\ - } > /etc/apache2/conf-available/remoteip.conf \ - && a2enconf remoteip \ -#Cleanup - && docker-php-source delete \ - && apt-get autoremove --purge -y \ - && apt-get clean \ - && rm -rf /var/cache/apt \ - && rm -rf /var/lib/apt/lists/ - -# Use the default production configuration -COPY contrib/docker/php.production.ini "$PHP_INI_DIR/php.ini" - -COPY . /var/www/ -# for detail why storage is copied this way, pls refer to https://github.com/pixelfed/pixelfed/pull/2137#discussion_r434468862 -RUN cp -r storage storage.skel \ - && composer install --prefer-dist --no-interaction --no-ansi --optimize-autoloader \ - && rm -rf html && ln -s public html \ - && chown -R www-data:www-data /var/www - -RUN php artisan horizon:publish - -VOLUME /var/www/storage /var/www/bootstrap - -CMD ["/var/www/contrib/docker/start.apache.sh"] diff --git a/contrib/docker/Dockerfile.fpm b/contrib/docker/Dockerfile.fpm deleted file mode 100644 index 1bb0a15f7..000000000 --- a/contrib/docker/Dockerfile.fpm +++ /dev/null @@ -1,90 +0,0 @@ -FROM php:8.1-fpm-bullseye - -ENV COMPOSER_MEMORY_LIMIT=-1 -ARG DEBIAN_FRONTEND=noninteractive -WORKDIR /var/www/ - -# Get Composer binary -COPY --from=composer:2.4.4 /usr/bin/composer /usr/bin/composer - -# Install package dependencies -RUN apt-get update \ - && apt-get upgrade -y \ -# && apt-get install -y --no-install-recommends apt-utils \ - && apt-get install -y --no-install-recommends \ -## Standard - locales \ - locales-all \ - git \ - gosu \ - zip \ - unzip \ - libzip-dev \ - libcurl4-openssl-dev \ -## Image Optimization - optipng \ - pngquant \ - jpegoptim \ - gifsicle \ -## Image Processing - libjpeg62-turbo-dev \ - libpng-dev \ - libmagickwand-dev \ -# Required for GD - libxpm4 \ - libxpm-dev \ - libwebp7 \ - libwebp-dev \ -## Video Processing - ffmpeg \ -## Database -# libpq-dev \ -# libsqlite3-dev \ - mariadb-client \ -# Locales Update - && sed -i '/en_US/s/^#//g' /etc/locale.gen \ - && locale-gen \ - && update-locale \ -# Install PHP extensions - && docker-php-source extract \ -#PHP Imagemagick extensions - && pecl install imagick \ - && docker-php-ext-enable imagick \ -# PHP GD extensions - && docker-php-ext-configure gd \ - --with-freetype \ - --with-jpeg \ - --with-webp \ - --with-xpm \ - && docker-php-ext-install -j$(nproc) gd \ -#PHP Redis extensions - && pecl install redis \ - && docker-php-ext-enable redis \ -#PHP Database extensions - && docker-php-ext-install pdo_mysql \ -#pdo_pgsql pdo_sqlite \ -#PHP extensions (dependencies) - && docker-php-ext-configure intl \ - && docker-php-ext-install -j$(nproc) intl bcmath zip pcntl exif curl \ -#Cleanup - && docker-php-source delete \ - && apt-get autoremove --purge -y \ - && apt-get clean \ - && rm -rf /var/cache/apt \ - && rm -rf /var/lib/apt/lists/ - -# Use the default production configuration -COPY contrib/docker/php.production.ini "$PHP_INI_DIR/php.ini" - -COPY . /var/www/ -# for detail why storage is copied this way, pls refer to https://github.com/pixelfed/pixelfed/pull/2137#discussion_r434468862 -RUN cp -r storage storage.skel \ - && composer install --prefer-dist --no-interaction --no-ansi --optimize-autoloader \ - && rm -rf html && ln -s public html \ - && chown -R www-data:www-data /var/www - -RUN php artisan horizon:publish - -VOLUME /var/www/storage /var/www/bootstrap - -CMD ["/var/www/contrib/docker/start.fpm.sh"] diff --git a/contrib/docker/start.apache.sh b/contrib/docker/start.apache.sh deleted file mode 100755 index 4fb19e476..000000000 --- a/contrib/docker/start.apache.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash - -# Create the storage tree if needed and fix permissions -cp -r storage.skel/* storage/ -chown -R www-data:www-data storage/ bootstrap/ - -# Refresh the environment -php artisan config:cache -php artisan storage:link -php artisan horizon:publish -php artisan route:cache -php artisan view:cache - -# Finally run Apache -apache2-foreground diff --git a/contrib/docker/start.fpm.sh b/contrib/docker/start.fpm.sh deleted file mode 100755 index 199489fc6..000000000 --- a/contrib/docker/start.fpm.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash - -# Create the storage tree if needed and fix permissions -cp -r storage.skel/* storage/ -chown -R www-data:www-data storage/ bootstrap/ - -# Refresh the environment -php artisan config:cache -php artisan storage:link -php artisan horizon:publish -php artisan route:cache -php artisan view:cache - -# Finally run FPM -php-fpm diff --git a/contrib/nginx.conf b/contrib/nginx.conf deleted file mode 100644 index 0f86ea9e7..000000000 --- a/contrib/nginx.conf +++ /dev/null @@ -1,67 +0,0 @@ -server { - listen 443 ssl http2; - listen [::]:443 ssl http2; - server_name pixelfed.example; # change this to your fqdn - root /home/pixelfed/public; # path to repo/public - - ssl_certificate /etc/nginx/ssl/server.crt; # generate your own - ssl_certificate_key /etc/nginx/ssl/server.key; # or use letsencrypt - - ssl_protocols TLSv1.2; - ssl_ciphers EECDH+AESGCM:EECDH+CHACHA20:EECDH+AES; - ssl_prefer_server_ciphers on; - - #add_header X-Frame-Options "SAMEORIGIN"; - add_header X-XSS-Protection "1; mode=block"; - add_header X-Content-Type-Options "nosniff"; - - index index.php; - - charset utf-8; - client_max_body_size 15M; - - location / { - try_files $uri $uri/ /index.php?$query_string; - } - - location = /favicon.ico { access_log off; log_not_found off; } - location = /robots.txt { access_log off; log_not_found off; } - - error_page 404 /index.php; - - location ~ \.php$ { - fastcgi_split_path_info ^(.+\.php)(/.+)$; - fastcgi_pass unix:/var/run/php/php8.1-fpm.sock; - fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; - fastcgi_param QUERY_STRING $query_string; - fastcgi_param REQUEST_METHOD $request_method; - fastcgi_param CONTENT_TYPE $content_type; - fastcgi_param CONTENT_LENGTH $content_length; - fastcgi_param SCRIPT_NAME $fastcgi_script_name; - fastcgi_param REQUEST_URI $request_uri; - fastcgi_param DOCUMENT_URI $document_uri; - fastcgi_param DOCUMENT_ROOT $document_root; - fastcgi_param SERVER_PROTOCOL $server_protocol; - fastcgi_param GATEWAY_INTERFACE CGI/1.1; - fastcgi_param SERVER_SOFTWARE nginx/$nginx_version; - fastcgi_param REMOTE_ADDR $remote_addr; - fastcgi_param REMOTE_PORT $remote_port; - fastcgi_param SERVER_ADDR $server_addr; - fastcgi_param SERVER_PORT $server_port; - fastcgi_param SERVER_NAME $server_name; - fastcgi_param HTTPS $https if_not_empty; - fastcgi_param REDIRECT_STATUS 200; - fastcgi_param HTTP_PROXY ""; - } - - location ~ /\.(?!well-known).* { - deny all; - } -} - -server { # Redirect http to https - server_name pixelfed.example; # change this to your fqdn - listen 80; - listen [::]:80; - return 301 https://$host$request_uri; -} diff --git a/docker-compose.migrate.yml b/docker-compose.migrate.yml new file mode 100644 index 000000000..b31771f27 --- /dev/null +++ b/docker-compose.migrate.yml @@ -0,0 +1,42 @@ +--- +version: "3" + +services: + migrate: + image: "secoresearch/rsync" + entrypoint: "" + working_dir: /migrate + command: 'bash -c "exit 1"' + restart: never + volumes: + ################################ + # Storage volume + ################################ + # OLD + - "app-storage:/migrate/app-storage/old" + # NEW + - "${DOCKER_APP_HOST_STORAGE_PATH}:/migrate/app-storage/new" + + ################################ + # MySQL/DB volume + ################################ + # OLD + - "db-data:/migrate/db-data/old" + # NEW + - "${DOCKER_DB_HOST_DATA_PATH}:/migrate/db-data/new" + + ################################ + # Redis volume + ################################ + # OLD + - "redis-data:/migrate/redis-data/old" + # NEW + - "${DOCKER_REDIS_HOST_DATA_PATH}:/migrate/redis-data/new" + +# Volumes from the old [docker-compose.yml] file +# https://github.com/pixelfed/pixelfed/blob/b1ff44ca2f75c088a11576fb03b5bad2fbed4d5c/docker-compose.yml#L72-L76 +volumes: + db-data: + redis-data: + app-storage: + app-bootstrap: diff --git a/docker-compose.yml b/docker-compose.yml index 6fca2eeb3..5df433c83 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,82 +1,218 @@ --- -version: '3' +# Require 3.8 to ensure people use a recent version of Docker + Compose +version: "3.8" -# In order to set configuration, please use a .env file in -# your compose project directory (the same directory as your -# docker-compose.yml), and set database options, application -# name, key, and other settings there. -# A list of available settings is available in .env.example -# -# The services should scale properly across a swarm cluster -# if the volumes are properly shared between cluster members. +############################################################### +# Please see docker/README.md for usage information +############################################################### services: -## App and Worker - app: - # Comment to use dockerhub image - image: pixelfed/pixelfed:latest + # HTTP/HTTPS proxy + # + # Sits in front of the *real* webserver and manages SSL and (optionally) + # load-balancing between multiple web servers + # + # You can disable this service by setting [DOCKER_PROXY_PROFILE="disabled"] + # in your [.env] file - the setting is near the bottom of the file. + # + # This also disables the [proxy-acme] service, if this is not desired, change the + # [DOCKER_PROXY_ACME_PROFILE] setting to an empty string [""] + # + # See: https://github.com/nginx-proxy/nginx-proxy/tree/main/docs + proxy: + image: nginxproxy/nginx-proxy:1.4 + container_name: "${DOCKER_ALL_CONTAINER_NAME_PREFIX}-proxy" restart: unless-stopped - env_file: - - .env.docker + profiles: + - ${DOCKER_PROXY_PROFILE:-} + environment: + DOCKER_SERVICE_NAME: "proxy" volumes: - - app-storage:/var/www/storage - - app-bootstrap:/var/www/bootstrap - - "./.env.docker:/var/www/.env" - networks: - - external - - internal + - "${DOCKER_PROXY_HOST_DOCKER_SOCKET_PATH}:/tmp/docker.sock:ro" + - "${DOCKER_ALL_HOST_CONFIG_ROOT_PATH}/proxy/conf.d:/etc/nginx/conf.d" + - "${DOCKER_ALL_HOST_CONFIG_ROOT_PATH}/proxy/vhost.d:/etc/nginx/vhost.d" + - "${DOCKER_ALL_HOST_CONFIG_ROOT_PATH}/proxy/certs:/etc/nginx/certs" + - "${DOCKER_ALL_HOST_DATA_ROOT_PATH}/proxy/html:/usr/share/nginx/html" ports: - - "8080:80" + - "${DOCKER_PROXY_HOST_PORT_HTTP}:80" + - "${DOCKER_PROXY_HOST_PORT_HTTPS}:443" + healthcheck: + test: "curl --fail https://${APP_DOMAIN}/api/service/health-check" + interval: "${DOCKER_PROXY_HEALTHCHECK_INTERVAL}" + retries: 2 + timeout: 5s + + # Proxy companion for managing letsencrypt SSL certificates + # + # You can disable this service by setting [DOCKER_PROXY_ACME_PROFILE="disabled"] + # in your [.env] file - the setting is near the bottom of the file. + # + # See: https://github.com/nginx-proxy/acme-companion/tree/main/docs + proxy-acme: + image: nginxproxy/acme-companion + container_name: "${DOCKER_ALL_CONTAINER_NAME_PREFIX}-proxy-acme" + restart: unless-stopped + profiles: + - ${DOCKER_PROXY_ACME_PROFILE:-} + environment: + DEBUG: 0 + DEFAULT_EMAIL: "${DOCKER_PROXY_LETSENCRYPT_EMAIL:?error}" + NGINX_PROXY_CONTAINER: "${DOCKER_ALL_CONTAINER_NAME_PREFIX}-proxy" + depends_on: + - proxy + volumes: + - "${DOCKER_ALL_HOST_CONFIG_ROOT_PATH}/proxy-acme:/etc/acme.sh" + - "${DOCKER_ALL_HOST_CONFIG_ROOT_PATH}/proxy/certs:/etc/nginx/certs" + - "${DOCKER_ALL_HOST_CONFIG_ROOT_PATH}/proxy/conf.d:/etc/nginx/conf.d" + - "${DOCKER_ALL_HOST_CONFIG_ROOT_PATH}/proxy/vhost.d:/etc/nginx/vhost.d" + - "${DOCKER_ALL_HOST_DATA_ROOT_PATH}/proxy/html:/usr/share/nginx/html" + - "${DOCKER_PROXY_HOST_DOCKER_SOCKET_PATH}:/var/run/docker.sock:ro" + + web: + image: "${DOCKER_APP_IMAGE}:${DOCKER_APP_TAG}" + container_name: "${DOCKER_ALL_CONTAINER_NAME_PREFIX}-web" + restart: unless-stopped + profiles: + - ${DOCKER_WEB_PROFILE:-} + build: + target: ${DOCKER_APP_RUNTIME}-runtime + cache_from: + - "type=registry,ref=${DOCKER_APP_IMAGE}-cache:${DOCKER_APP_TAG}" + args: + APT_PACKAGES_EXTRA: "${DOCKER_APP_APT_PACKAGES_EXTRA:-}" + PHP_BASE_TYPE: "${DOCKER_APP_BASE_TYPE}" + PHP_DEBIAN_RELEASE: "${DOCKER_APP_DEBIAN_RELEASE}" + PHP_EXTENSIONS_EXTRA: "${DOCKER_APP_PHP_EXTENSIONS_EXTRA:-}" + PHP_PECL_EXTENSIONS_EXTRA: "${DOCKER_APP_PHP_PECL_EXTENSIONS_EXTRA:-}" + PHP_VERSION: "${DOCKER_APP_PHP_VERSION:?error}" + environment: + # Used by Pixelfed Docker init script + DOCKER_SERVICE_NAME: "web" + DOCKER_APP_ENTRYPOINT_DEBUG: ${DOCKER_APP_ENTRYPOINT_DEBUG:-0} + ENTRYPOINT_SKIP_SCRIPTS: ${ENTRYPOINT_SKIP_SCRIPTS:-} + # Used by [proxy] service + LETSENCRYPT_HOST: "${DOCKER_PROXY_LETSENCRYPT_HOST:?error}" + LETSENCRYPT_EMAIL: "${DOCKER_PROXY_LETSENCRYPT_EMAIL:?error}" + LETSENCRYPT_TEST: "${DOCKER_PROXY_LETSENCRYPT_TEST:-}" + VIRTUAL_HOST: "${APP_DOMAIN}" + VIRTUAL_PORT: "80" + volumes: + - "./.env:/var/www/.env" + - "${DOCKER_ALL_HOST_CONFIG_ROOT_PATH}/proxy/conf.d:/shared/proxy/conf.d" + - "${DOCKER_APP_HOST_CACHE_PATH}:/var/www/bootstrap/cache" + - "${DOCKER_APP_HOST_OVERRIDES_PATH}:/docker/overrides:ro" + - "${DOCKER_APP_HOST_STORAGE_PATH}:/var/www/storage" + labels: + com.github.nginx-proxy.nginx-proxy.keepalive: 30 + com.github.nginx-proxy.nginx-proxy.http2.enable: true + com.github.nginx-proxy.nginx-proxy.http3.enable: true + ports: + - "${DOCKER_WEB_PORT_EXTERNAL_HTTP}:80" depends_on: - db - redis + healthcheck: + test: 'curl --header "Host: ${APP_DOMAIN}" --fail http://localhost/api/service/health-check' + interval: "${DOCKER_WEB_HEALTHCHECK_INTERVAL}" + retries: 2 + timeout: 5s worker: - image: pixelfed/pixelfed:latest - restart: unless-stopped - env_file: - - .env.docker - volumes: - - app-storage:/var/www/storage - - app-bootstrap:/var/www/bootstrap - networks: - - external - - internal + image: "${DOCKER_APP_IMAGE}:${DOCKER_APP_TAG}" + container_name: "${DOCKER_ALL_CONTAINER_NAME_PREFIX}-worker" command: gosu www-data php artisan horizon + restart: unless-stopped + stop_signal: SIGTERM + profiles: + - ${DOCKER_WORKER_PROFILE:-} + build: + target: ${DOCKER_APP_RUNTIME}-runtime + cache_from: + - "type=registry,ref=${DOCKER_APP_IMAGE}-cache:${DOCKER_APP_TAG}" + args: + APT_PACKAGES_EXTRA: "${DOCKER_APP_APT_PACKAGES_EXTRA:-}" + PHP_BASE_TYPE: "${DOCKER_APP_BASE_TYPE}" + PHP_DEBIAN_RELEASE: "${DOCKER_APP_DEBIAN_RELEASE}" + PHP_EXTENSIONS_EXTRA: "${DOCKER_APP_PHP_EXTENSIONS_EXTRA:-}" + PHP_PECL_EXTENSIONS_EXTRA: "${DOCKER_APP_PHP_PECL_EXTENSIONS_EXTRA:-}" + PHP_VERSION: "${DOCKER_APP_PHP_VERSION:?error}" + environment: + # Used by Pixelfed Docker init script + DOCKER_SERVICE_NAME: "worker" + DOCKER_APP_ENTRYPOINT_DEBUG: ${DOCKER_APP_ENTRYPOINT_DEBUG:-0} + ENTRYPOINT_SKIP_SCRIPTS: ${ENTRYPOINT_SKIP_SCRIPTS:-} + volumes: + - "./.env:/var/www/.env" + - "${DOCKER_ALL_HOST_CONFIG_ROOT_PATH}/proxy/conf.d:/shared/proxy/conf.d" + - "${DOCKER_APP_HOST_CACHE_PATH}:/var/www/bootstrap/cache" + - "${DOCKER_APP_HOST_OVERRIDES_PATH}:/docker/overrides:ro" + - "${DOCKER_APP_HOST_STORAGE_PATH}:/var/www/storage" depends_on: - db - redis + healthcheck: + test: gosu www-data php artisan horizon:status | grep running + interval: "${DOCKER_WORKER_HEALTHCHECK_INTERVAL:?error}" + timeout: 5s + retries: 2 -## DB and Cache db: - image: mariadb:jammy + image: ${DOCKER_DB_IMAGE:?error} + container_name: "${DOCKER_ALL_CONTAINER_NAME_PREFIX}-db" + command: ${DOCKER_DB_COMMAND:-} restart: unless-stopped - networks: - - internal - command: --default-authentication-plugin=mysql_native_password - env_file: - - .env.docker + profiles: + - ${DOCKER_DB_PROFILE:-} + environment: + TZ: "${TZ:?error}" + # MySQL (Oracle) - "Environment Variables" at https://hub.docker.com/_/mysql + MYSQL_ROOT_PASSWORD: "${DB_PASSWORD:?error}" + MYSQL_USER: "${DB_USERNAME:?error}" + MYSQL_PASSWORD: "${DB_PASSWORD:?error}" + MYSQL_DATABASE: "${DB_DATABASE:?error}" + # MySQL (MariaDB) - "Start a mariadb server instance with user, password and database" at https://hub.docker.com/_/mariadb + MARIADB_ROOT_PASSWORD: "${DB_PASSWORD:?error}" + MARIADB_USER: "${DB_USERNAME:?error}" + MARIADB_PASSWORD: "${DB_PASSWORD:?error}" + MARIADB_DATABASE: "${DB_DATABASE:?error}" + # PostgreSQL - "Environment Variables" at https://hub.docker.com/_/postgres + POSTGRES_USER: "${DB_USERNAME:?error}" + POSTGRES_PASSWORD: "${DB_PASSWORD:?error}" + POSTGRES_DB: "${DB_DATABASE:?error}" volumes: - - "db-data:/var/lib/mysql" + - "${DOCKER_DB_HOST_DATA_PATH:?error}:${DOCKER_DB_CONTAINER_DATA_PATH:?error}" + ports: + - "${DOCKER_DB_HOST_PORT:?error}:${DOCKER_DB_CONTAINER_PORT:?error}" + healthcheck: + test: + [ + "CMD", + "healthcheck.sh", + "--su-mysql", + "--connect", + "--innodb_initialized", + ] + interval: "${DOCKER_DB_HEALTHCHECK_INTERVAL:?error}" + retries: 2 + timeout: 5s redis: - image: redis:5-alpine + image: redis:${DOCKER_REDIS_VERSION} + container_name: "${DOCKER_ALL_CONTAINER_NAME_PREFIX}-redis" restart: unless-stopped - env_file: - - .env.docker + command: "${DOCKER_REDIS_CONFIG_FILE:-} --requirepass '${REDIS_PASSWORD:-}'" + profiles: + - ${DOCKER_REDIS_PROFILE:-} + environment: + TZ: "${TZ:?error}" + REDISCLI_AUTH: ${REDIS_PASSWORD:-} volumes: - - "redis-data:/data" - networks: - - internal - -volumes: - db-data: - redis-data: - app-storage: - app-bootstrap: - -networks: - internal: - internal: true - external: - driver: bridge + - "${DOCKER_ALL_HOST_CONFIG_ROOT_PATH}/redis:/etc/redis" + - "${DOCKER_REDIS_HOST_DATA_PATH}:/data" + ports: + - "${DOCKER_REDIS_HOST_PORT}:6379" + healthcheck: + test: ["CMD", "redis-cli", "-p", "6379", "ping"] + interval: "${DOCKER_REDIS_HEALTHCHECK_INTERVAL:?error}" + retries: 2 + timeout: 5s diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 000000000..5230f60fd --- /dev/null +++ b/docker/README.md @@ -0,0 +1,5 @@ +# Pixelfed + Docker + Docker Compose + +Please see the [Pixelfed Docs (Next)](https://jippi.github.io/pixelfed-docs-next/pr-preview/pr-1/running-pixelfed/) for current documentation on Docker usage. + +The docs can be [reviewed in the pixelfed/docs-next](https://github.com/pixelfed/docs-next/pull/1) repository. diff --git a/docker/apache/root/etc/apache2/conf-available/remoteip.conf b/docker/apache/root/etc/apache2/conf-available/remoteip.conf new file mode 100644 index 000000000..516da9f5d --- /dev/null +++ b/docker/apache/root/etc/apache2/conf-available/remoteip.conf @@ -0,0 +1,8 @@ +RemoteIPHeader X-Real-IP + +# All private IPs as outlined in rfc1918 +# +# See: https://datatracker.ietf.org/doc/html/rfc1918 +RemoteIPTrustedProxy 10.0.0.0/8 +RemoteIPTrustedProxy 172.16.0.0/12 +RemoteIPTrustedProxy 192.168.0.0/16 diff --git a/docker/artisan b/docker/artisan new file mode 100755 index 000000000..3bbf58aea --- /dev/null +++ b/docker/artisan @@ -0,0 +1,11 @@ +#!/bin/bash + +declare service="${PF_SERVICE:=worker}" +declare user="${PF_USER:=www-data}" + +exec docker compose exec \ + --user "${user}" \ + --env TERM \ + --env COLORTERM \ + "${service}" \ + php artisan "${@}" diff --git a/docker/dottie b/docker/dottie new file mode 100755 index 000000000..8bd304a03 --- /dev/null +++ b/docker/dottie @@ -0,0 +1,45 @@ +#!/bin/bash + +set -e -o errexit -o nounset -o pipefail + +declare project_root="${PWD}" +declare user="${PF_USER:=www-data}" + +if command -v git &>/dev/null; then + project_root=$(git rev-parse --show-toplevel) +fi + +declare -r release="${DOTTIE_VERSION:-latest}" + +declare -r update_check_file="/tmp/.dottie-update-check" # file to check age of since last update +declare -i update_check_max_age=$((8 * 60 * 60)) # 8 hours between checking for dottie version +declare -i update_check_cur_age=$((update_check_max_age + 1)) # by default the "update" event should happen + +# default [docker run] flags +declare -a flags=( + --rm + --interactive + --tty + --user "${user}" + --env TERM + --env COLORTERM + --volume "${project_root}:/var/www" + --workdir /var/www +) + +# if update file exists, find its age since last modification +if [[ -f "${update_check_file}" ]]; then + now=$(date +%s) + changed=$(date -r "${update_check_file}" +%s) + update_check_cur_age=$((now - changed)) +fi + +# if update file is older than max allowed poll for new version of dottie +if [[ $update_check_cur_age -gt $update_check_max_age ]]; then + flags+=(--pull always) + + touch "${update_check_file}" +fi + +# run dottie +exec docker run "${flags[@]}" "ghcr.io/jippi/dottie:${release}" "$@" diff --git a/docker/fpm/root/.gitkeep b/docker/fpm/root/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/docker/nginx/Procfile b/docker/nginx/Procfile new file mode 100644 index 000000000..bd375bf6a --- /dev/null +++ b/docker/nginx/Procfile @@ -0,0 +1,2 @@ +fpm: php-fpm +nginx: nginx -g "daemon off;" diff --git a/docker/nginx/root/docker/templates/etc/nginx/conf.d/default.conf b/docker/nginx/root/docker/templates/etc/nginx/conf.d/default.conf new file mode 100644 index 000000000..15bf17beb --- /dev/null +++ b/docker/nginx/root/docker/templates/etc/nginx/conf.d/default.conf @@ -0,0 +1,49 @@ +server { + listen 80 default_server; + + server_name {{ getenv "APP_DOMAIN" }}; + root /var/www/public; + + add_header X-Frame-Options "SAMEORIGIN"; + add_header X-XSS-Protection "1; mode=block"; + add_header X-Content-Type-Options "nosniff"; + + access_log /dev/stdout; + error_log /dev/stderr warn; + + index index.html index.htm index.php; + + charset utf-8; + client_max_body_size {{ getenv "POST_MAX_SIZE" "61M" }}; + + location / { + try_files $uri $uri/ /index.php?$query_string; + } + + location = /favicon.ico { + access_log off; + log_not_found off; + } + + location = /robots.txt { + access_log off; + log_not_found off; + } + + error_page 404 /index.php; + + location ~ \.php$ { + fastcgi_split_path_info ^(.+\.php)(/.+)$; + + fastcgi_pass 127.0.0.1:9000; + fastcgi_index index.php; + + include fastcgi_params; + + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + } + + location ~ /\.(?!well-known).* { + deny all; + } +} diff --git a/docker/nginx/root/docker/templates/etc/nginx/nginx.conf b/docker/nginx/root/docker/templates/etc/nginx/nginx.conf new file mode 100644 index 000000000..4e87a4565 --- /dev/null +++ b/docker/nginx/root/docker/templates/etc/nginx/nginx.conf @@ -0,0 +1,41 @@ +# This is changed from the original "nginx" in upstream to work properly +# with permissions within pixelfed when serving static files. +user www-data; + +worker_processes auto; + +# Ensure the PID is writable +# Lifted from: https://hub.docker.com/r/nginxinc/nginx-unprivileged +pid /tmp/nginx.pid; + +# Write error log to stderr (/proc/self/fd/2 -> /dev/stderr) +error_log /proc/self/fd/2 notice; + +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent "$http_referer" "$http_user_agent" "$http_x_forwarded_for"'; + + # Write error log to stdout (/proc/self/fd/1 -> /dev/stdout) + access_log /proc/self/fd/1 main; + + sendfile on; + tcp_nopush on; + keepalive_timeout 65; + gzip on; + + # Ensure all temp paths are in a writable by "www-data" user. + # Lifted from: https://hub.docker.com/r/nginxinc/nginx-unprivileged + client_body_temp_path /tmp/client_temp; + proxy_temp_path /tmp/proxy_temp_path; + fastcgi_temp_path /tmp/fastcgi_temp; + uwsgi_temp_path /tmp/uwsgi_temp; + scgi_temp_path /tmp/scgi_temp; + + include /etc/nginx/conf.d/*.conf; +} diff --git a/docker/shared/root/docker/entrypoint.d/01-permissions.sh b/docker/shared/root/docker/entrypoint.d/01-permissions.sh new file mode 100755 index 000000000..efff58110 --- /dev/null +++ b/docker/shared/root/docker/entrypoint.d/01-permissions.sh @@ -0,0 +1,31 @@ +#!/bin/bash +: "${ENTRYPOINT_ROOT:="/docker"}" + +# shellcheck source=SCRIPTDIR/../helpers.sh +source "${ENTRYPOINT_ROOT}/helpers.sh" + +entrypoint-set-script-name "$0" + +# Ensure the Docker volumes and required files are owned by the runtime user as other scripts +# will be writing to these +run-as-current-user chown --verbose "${RUNTIME_UID}:${RUNTIME_GID}" "./.env" +run-as-current-user chown --verbose "${RUNTIME_UID}:${RUNTIME_GID}" "./bootstrap/cache" +run-as-current-user chown --verbose "${RUNTIME_UID}:${RUNTIME_GID}" "./storage" +run-as-current-user chown --verbose --recursive "${RUNTIME_UID}:${RUNTIME_GID}" "./storage/docker" + +# Optionally fix ownership of configured paths +: "${DOCKER_APP_ENSURE_OWNERSHIP_PATHS:=""}" + +declare -a ensure_ownership_paths=() +IFS=' ' read -ar ensure_ownership_paths <<<"${DOCKER_APP_ENSURE_OWNERSHIP_PATHS}" + +if [[ ${#ensure_ownership_paths[@]} == 0 ]]; then + log-info "No paths has been configured for ownership fixes via [\$DOCKER_APP_ENSURE_OWNERSHIP_PATHS]." + + exit 0 +fi + +for path in "${ensure_ownership_paths[@]}"; do + log-info "Ensure ownership of [${path}] is correct" + stream-prefix-command-output run-as-current-user chown --recursive "${RUNTIME_UID}:${RUNTIME_GID}" "${path}" +done diff --git a/docker/shared/root/docker/entrypoint.d/02-check-config.sh b/docker/shared/root/docker/entrypoint.d/02-check-config.sh new file mode 100755 index 000000000..627960352 --- /dev/null +++ b/docker/shared/root/docker/entrypoint.d/02-check-config.sh @@ -0,0 +1,21 @@ +#!/bin/bash +: "${ENTRYPOINT_ROOT:="/docker"}" + +# shellcheck source=SCRIPTDIR/../helpers.sh +source "${ENTRYPOINT_ROOT}/helpers.sh" + +entrypoint-set-script-name "$0" + +# Validating dot-env files for any issues +for file in "${dot_env_files[@]}"; do + if ! file-exists "${file}"; then + log-warning "Could not source file [${file}]: does not exists" + continue + fi + + # We ignore 'dir' + 'file' rules since they are validate *host* paths + # which do not (and should not) exists inside the container + # + # We disable fixer since its not interactive anyway + run-as-current-user dottie validate --file "${file}" --ignore-rule dir,file --exclude-prefix APP_KEY --no-fix +done diff --git a/docker/shared/root/docker/entrypoint.d/04-defaults.envsh b/docker/shared/root/docker/entrypoint.d/04-defaults.envsh new file mode 100755 index 000000000..a55a56e6c --- /dev/null +++ b/docker/shared/root/docker/entrypoint.d/04-defaults.envsh @@ -0,0 +1,33 @@ +#!/bin/bash + +# NOTE: +# +# This file is *sourced* not run by the entrypoint runner +# so any environment values set here will be accessible to all sub-processes +# and future entrypoint.d scripts +# +# We also don't need to source `helpers.sh` since it's already available + +entrypoint-set-script-name "${BASH_SOURCE[0]}" + +load-config-files + +: "${MAX_PHOTO_SIZE:=15000}" +: "${MAX_ALBUM_LENGTH:=4}" + +# We assign a 1MB buffer to the just-in-time calculated max post size to allow for fields and overhead +: "${POST_MAX_SIZE_BUFFER:=1M}" +log-info "POST_MAX_SIZE_BUFFER is set to [${POST_MAX_SIZE_BUFFER}]" +buffer=$(numfmt --invalid=fail --from=auto --to=none --to-unit=K "${POST_MAX_SIZE_BUFFER}") +log-info "POST_MAX_SIZE_BUFFER converted to KB is [${buffer}]" + +# Automatically calculate the [post_max_size] value for [php.ini] and [nginx] +log-info "POST_MAX_SIZE will be calculated by [({MAX_PHOTO_SIZE} * {MAX_ALBUM_LENGTH}) + {POST_MAX_SIZE_BUFFER}]" +log-info " MAX_PHOTO_SIZE=${MAX_PHOTO_SIZE}" +log-info " MAX_ALBUM_LENGTH=${MAX_ALBUM_LENGTH}" +log-info " POST_MAX_SIZE_BUFFER=${buffer}" +: "${POST_MAX_SIZE:=$(numfmt --invalid=fail --from=auto --from-unit=K --to=si $(((MAX_PHOTO_SIZE * MAX_ALBUM_LENGTH) + buffer)))}" +log-info "POST_MAX_SIZE was calculated to [${POST_MAX_SIZE}]" + +# NOTE: must export the value so it's available in other scripts! +export POST_MAX_SIZE diff --git a/docker/shared/root/docker/entrypoint.d/05-templating.sh b/docker/shared/root/docker/entrypoint.d/05-templating.sh new file mode 100755 index 000000000..4d229b11c --- /dev/null +++ b/docker/shared/root/docker/entrypoint.d/05-templating.sh @@ -0,0 +1,60 @@ +#!/bin/bash +: "${ENTRYPOINT_ROOT:="/docker"}" + +# shellcheck source=SCRIPTDIR/../helpers.sh +source "${ENTRYPOINT_ROOT}/helpers.sh" + +entrypoint-set-script-name "$0" + +# Show [git diff] of templates being rendered (will help verify output) +: "${ENTRYPOINT_SHOW_TEMPLATE_DIFF:=1}" +# Directory where templates can be found +: "${ENTRYPOINT_TEMPLATE_DIR:=/docker/templates/}" +# Root path to write template template_files to (default is '', meaning it will be written to /) +: "${ENTRYPOINT_TEMPLATE_OUTPUT_PREFIX:=}" + +declare template_file relative_template_file_path output_file_dir + +# load all dot-env config files +load-config-files + +# export all dot-env variables so they are available in templating +# +# shellcheck disable=SC2068 +export ${seen_dot_env_variables[@]} + +find "${ENTRYPOINT_TEMPLATE_DIR}" -follow -type f -print | while read -r template_file; do + # Example: template_file=/docker/templates/usr/local/etc/php/php.ini + + # The file path without the template dir prefix ($ENTRYPOINT_TEMPLATE_DIR) + # + # Example: /usr/local/etc/php/php.ini + relative_template_file_path="${template_file#"${ENTRYPOINT_TEMPLATE_DIR}"}" + + # Adds optional prefix to the output file path + # + # Example: /usr/local/etc/php/php.ini + output_file_path="${ENTRYPOINT_TEMPLATE_OUTPUT_PREFIX}/${relative_template_file_path}" + + # Remove the file from the path + # + # Example: /usr/local/etc/php + output_file_dir=$(dirname "${output_file_path}") + + # Ensure the output directory is writable + if ! is-writable "${output_file_dir}"; then + log-error-and-exit "${output_file_dir} is not writable" + fi + + # Create the output directory if it doesn't exists + ensure-directory-exists "${output_file_dir}" + + # Render the template + log-info "Running [gomplate] on [${template_file}] --> [${output_file_path}]" + gomplate <"${template_file}" >"${output_file_path}" + + # Show the diff from the envsubst command + if is-true "${ENTRYPOINT_SHOW_TEMPLATE_DIFF}"; then + git --no-pager diff --color=always "${template_file}" "${output_file_path}" || : # ignore diff exit code + fi +done diff --git a/docker/shared/root/docker/entrypoint.d/10-storage.sh b/docker/shared/root/docker/entrypoint.d/10-storage.sh new file mode 100755 index 000000000..54145a365 --- /dev/null +++ b/docker/shared/root/docker/entrypoint.d/10-storage.sh @@ -0,0 +1,13 @@ +#!/bin/bash +: "${ENTRYPOINT_ROOT:="/docker"}" + +# shellcheck source=SCRIPTDIR/../helpers.sh +source "${ENTRYPOINT_ROOT}/helpers.sh" + +entrypoint-set-script-name "$0" + +# Copy the [storage/] skeleton files over the "real" [storage/] directory so assets are updated between versions +run-as-runtime-user cp --force --recursive storage.skel/. ./storage/ + +# Ensure storage linkk are correctly configured +run-as-runtime-user php artisan storage:link diff --git a/docker/shared/root/docker/entrypoint.d/11-first-time-setup.sh b/docker/shared/root/docker/entrypoint.d/11-first-time-setup.sh new file mode 100755 index 000000000..fb5c86a39 --- /dev/null +++ b/docker/shared/root/docker/entrypoint.d/11-first-time-setup.sh @@ -0,0 +1,38 @@ +#!/bin/bash +: "${ENTRYPOINT_ROOT:="/docker"}" + +# shellcheck source=SCRIPTDIR/../helpers.sh +source "${ENTRYPOINT_ROOT}/helpers.sh" + +entrypoint-set-script-name "$0" + +load-config-files + +# Allow automatic applying of outstanding/new migrations on startup +: "${DOCKER_APP_RUN_ONE_TIME_SETUP_TASKS:=1}" + +if is-false "${DOCKER_APP_RUN_ONE_TIME_SETUP_TASKS}"; then + log-warning "Automatic run of the 'One-time setup tasks' is disabled." + log-warning "Please set [DOCKER_APP_RUN_ONE_TIME_SETUP_TASKS=1] in your [.env] file to enable this." + + exit 0 +fi + +await-database-ready + +# Following https://docs.pixelfed.org/running-pixelfed/installation/#one-time-setup-tasks +# +# NOTE: Caches happens in [30-cache.sh] + +only-once "key:generate" run-as-runtime-user php artisan key:generate +only-once "storage:link" run-as-runtime-user php artisan storage:link +only-once "initial:migrate" run-as-runtime-user php artisan migrate --force +only-once "import:cities" run-as-runtime-user php artisan import:cities + +if is-true "${ACTIVITY_PUB:-false}"; then + only-once "instance:actor" run-as-runtime-user php artisan instance:actor +fi + +if is-true "${OAUTH_ENABLED:-false}"; then + only-once "passport:keys" run-as-runtime-user php artisan passport:keys +fi diff --git a/docker/shared/root/docker/entrypoint.d/12-migrations.sh b/docker/shared/root/docker/entrypoint.d/12-migrations.sh new file mode 100755 index 000000000..3b87daf1f --- /dev/null +++ b/docker/shared/root/docker/entrypoint.d/12-migrations.sh @@ -0,0 +1,42 @@ +#!/bin/bash +: "${ENTRYPOINT_ROOT:="/docker"}" + +# shellcheck source=SCRIPTDIR/../helpers.sh +source "${ENTRYPOINT_ROOT}/helpers.sh" + +entrypoint-set-script-name "$0" + +# Allow automatic applying of outstanding/new migrations on startup +: "${DB_APPLY_NEW_MIGRATIONS_AUTOMATICALLY:=0}" + +# Wait for the database to be ready +await-database-ready + +# Run the migrate:status command and capture output +output=$(run-as-runtime-user php artisan migrate:status || :) + +# By default we have no new migrations +declare -i new_migrations=0 + +# Detect if any new migrations are available by checking for "No" in the output +echo "$output" | grep No && new_migrations=1 + +if is-false "${new_migrations}"; then + log-info "No new migrations detected" + + exit 0 +fi + +log-warning "New migrations available" + +# Print the output +echo "$output" + +if is-false "${DB_APPLY_NEW_MIGRATIONS_AUTOMATICALLY}"; then + log-info "Automatic applying of new database migrations is disabled" + log-info "Please set [DB_APPLY_NEW_MIGRATIONS_AUTOMATICALLY=1] in your [.env] file to enable this." + + exit 0 +fi + +run-as-runtime-user php artisan migrate --force diff --git a/docker/shared/root/docker/entrypoint.d/20-horizon.sh b/docker/shared/root/docker/entrypoint.d/20-horizon.sh new file mode 100755 index 000000000..55efd768d --- /dev/null +++ b/docker/shared/root/docker/entrypoint.d/20-horizon.sh @@ -0,0 +1,9 @@ +#!/bin/bash +: "${ENTRYPOINT_ROOT:="/docker"}" + +# shellcheck source=SCRIPTDIR/../helpers.sh +source "${ENTRYPOINT_ROOT}/helpers.sh" + +entrypoint-set-script-name "$0" + +run-as-runtime-user php artisan horizon:publish diff --git a/docker/shared/root/docker/entrypoint.d/30-cache.sh b/docker/shared/root/docker/entrypoint.d/30-cache.sh new file mode 100755 index 000000000..c970db60b --- /dev/null +++ b/docker/shared/root/docker/entrypoint.d/30-cache.sh @@ -0,0 +1,11 @@ +#!/bin/bash +: "${ENTRYPOINT_ROOT:="/docker"}" + +# shellcheck source=SCRIPTDIR/../helpers.sh +source "${ENTRYPOINT_ROOT}/helpers.sh" + +entrypoint-set-script-name "$0" + +run-as-runtime-user php artisan config:cache +run-as-runtime-user php artisan route:cache +run-as-runtime-user php artisan view:cache diff --git a/docker/shared/root/docker/entrypoint.sh b/docker/shared/root/docker/entrypoint.sh new file mode 100755 index 000000000..73f6a4f3e --- /dev/null +++ b/docker/shared/root/docker/entrypoint.sh @@ -0,0 +1,105 @@ +#!/bin/bash +# short curcuit the entrypoint if $ENTRYPOINT_SKIP isn't set to 0 +if [[ ${ENTRYPOINT_SKIP:=0} != 0 ]]; then + exec "$@" +fi + +: "${ENTRYPOINT_ROOT:="/docker"}" +export ENTRYPOINT_ROOT + +# Directory where entrypoint scripts lives +: "${ENTRYPOINT_D_ROOT:="${ENTRYPOINT_ROOT}/entrypoint.d/"}" +export ENTRYPOINT_D_ROOT + +: "${DOCKER_APP_HOST_OVERRIDES_PATH:="${ENTRYPOINT_ROOT}/overrides"}" +export DOCKER_APP_HOST_OVERRIDES_PATH + +# Space separated list of scripts the entrypoint runner should skip +: "${ENTRYPOINT_SKIP_SCRIPTS:=""}" + +# Load helper scripts +# +# shellcheck source=SCRIPTDIR/helpers.sh +source "${ENTRYPOINT_ROOT}/helpers.sh" + +# Set the entrypoint name for logging +entrypoint-set-script-name "entrypoint.sh" + +# Convert ENTRYPOINT_SKIP_SCRIPTS into a native bash array for easier lookup +declare -a skip_scripts +# shellcheck disable=SC2034 +IFS=' ' read -ar skip_scripts <<< "$ENTRYPOINT_SKIP_SCRIPTS" + +# Ensure the entrypoint root folder exists +mkdir -p "${ENTRYPOINT_D_ROOT}" + +# If ENTRYPOINT_D_ROOT directory is empty, warn and run the regular command +if directory-is-empty "${ENTRYPOINT_D_ROOT}"; then + log-warning "No files found in ${ENTRYPOINT_D_ROOT}, skipping configuration" + + exec "$@" +fi + +# If the overridess directory exists, then copy all files into the container +if ! directory-is-empty "${DOCKER_APP_HOST_OVERRIDES_PATH}"; then + log-info "Overrides directory is not empty, copying files" + run-as-current-user cp --verbose --recursive "${DOCKER_APP_HOST_OVERRIDES_PATH}/." / +fi + +acquire-lock "entrypoint.sh" + +# Start scanning for entrypoint.d files to source or run +log-info "looking for shell scripts in [${ENTRYPOINT_D_ROOT}]" + +find "${ENTRYPOINT_D_ROOT}" -follow -type f -print | sort -V | while read -r file; do + # Skip the script if it's in the skip-script list + if in-array "$(get-entrypoint-script-name "${file}")" skip_scripts; then + log-warning "Skipping script [${file}] since it's in the skip list (\$ENTRYPOINT_SKIP_SCRIPTS)" + + continue + fi + + # Inspect the file extension of the file we're processing + case "${file}" in + *.envsh) + if ! is-executable "${file}"; then + # warn on shell scripts without exec bit + log-error-and-exit "File [${file}] is not executable (please 'chmod +x' it)" + fi + + log-info "${section_message_color}============================================================${color_clear}" + log-info "${section_message_color}Sourcing [${file}]${color_clear}" + log-info "${section_message_color}============================================================${color_clear}" + + # shellcheck disable=SC1090 + source "${file}" + + # the sourced file will (should) than the log prefix, so this restores our own + # "global" log prefix once the file is done being sourced + entrypoint-restore-script-name + ;; + + *.sh) + if ! is-executable "${file}"; then + # warn on shell scripts without exec bit + log-error-and-exit "File [${file}] is not executable (please 'chmod +x' it)" + fi + + log-info "${section_message_color}============================================================${color_clear}" + log-info "${section_message_color}Executing [${file}]${color_clear}" + log-info "${section_message_color}============================================================${color_clear}" + + "${file}" + ;; + + *) + log-warning "Ignoring unrecognized file [${file}]" + ;; + esac +done + +release-lock "entrypoint.sh" + +log-info "Configuration complete; ready for start up" + +exec "$@" diff --git a/docker/shared/root/docker/helpers.sh b/docker/shared/root/docker/helpers.sh new file mode 100644 index 000000000..3a21ee89e --- /dev/null +++ b/docker/shared/root/docker/helpers.sh @@ -0,0 +1,593 @@ +#!/bin/bash +set -e -o errexit -o nounset -o pipefail + +[[ ${DOCKER_APP_ENTRYPOINT_DEBUG:=0} == 1 ]] && set -x + +: "${RUNTIME_UID:="33"}" +: "${RUNTIME_GID:="33"}" + +# Some splash of color for important messages +declare -g error_message_color="\033[1;31m" +declare -g warn_message_color="\033[1;33m" +declare -g notice_message_color="\033[1;34m" +declare -g success_message_color="\033[1;32m" +# shellcheck disable=SC2034 +declare -g section_message_color="\033[1;35m" +declare -g color_clear="\033[1;0m" + +# Current and previous log prefix +declare -g script_name= +declare -g script_name_previous= +declare -g log_prefix= + +declare -Ag lock_fds=() + +# dot-env files to source when reading config +declare -a dot_env_files=( + /var/www/.env +) + +# environment keys seen when source dot files (so we can [export] them) +declare -ga seen_dot_env_variables=() + +declare -g docker_state_path +docker_state_path="$(readlink -f ./storage/docker)" + +declare -g docker_locks_path="${docker_state_path}/lock" +declare -g docker_once_path="${docker_state_path}/once" + +declare -g runtime_username +runtime_username=$(id -un "${RUNTIME_UID}") + +# We should already be in /var/www, but just to be explicit +cd /var/www || log-error-and-exit "could not change to /var/www" + +# @description Restore the log prefix to the previous value that was captured in [entrypoint-set-script-name ] +# @arg $1 string The name (or path) of the entrypoint script being run +function entrypoint-set-script-name() +{ + script_name_previous="${script_name}" + script_name="${1}" + + log_prefix="[entrypoint / $(get-entrypoint-script-name "$1")] - " +} + +# @description Restore the log prefix to the previous value that was captured in [entrypoint-set-script-name ] +function entrypoint-restore-script-name() +{ + entrypoint-set-script-name "${script_name_previous}" +} + +# @description Run a command as the [runtime user] +# @arg $@ string The command to run +# @exitcode 0 if the command succeeeds +# @exitcode 1 if the command fails +function run-as-runtime-user() +{ + run-command-as "${runtime_username}" "${@}" +} + +# @description Run a command as the [runtime user] +# @arg $@ string The command to run +# @exitcode 0 if the command succeeeds +# @exitcode 1 if the command fails +function run-as-current-user() +{ + run-command-as "$(id -un)" "${@}" +} + +# @description Run a command as the a named user +# @arg $1 string The user to run the command as +# @arg $@ string The command to run +# @exitcode 0 If the command succeeeds +# @exitcode 1 If the command fails +function run-command-as() +{ + local -i exit_code + local target_user + + target_user=${1} + shift + + log-info-stderr "${notice_message_color}👷 Running [${*}] as [${target_user}]${color_clear}" + + # disable error on exit behavior temporarily while we run the command + set +e + + if [[ ${target_user} != "root" ]]; then + stream-prefix-command-output su --preserve-environment "${target_user}" --shell /bin/bash --command "${*}" + else + stream-prefix-command-output "${@}" + fi + + # capture exit code + exit_code=$? + + # re-enable exit code handling + set -e + + if [[ $exit_code != 0 ]]; then + log-error "${error_message_color}❌ Error!${color_clear}" + + return "$exit_code" + fi + + log-info-stderr "${success_message_color}✅ OK!${color_clear}" + + return "$exit_code" +} + +# @description Streams stdout from the command and echo it +# with log prefixing. +# @see stream-prefix-command-output +function stream-stdout-handler() +{ + while read -r line; do + log-info "(stdout) ${line}" + done +} + +# @description Streams stderr from the command and echo it +# with a bit of color and log prefixing. +# @see stream-prefix-command-output +function stream-stderr-handler() +{ + while read -r line; do + log-info-stderr "(${error_message_color}stderr${color_clear}) ${line}" + done +} + +# @description Steam stdout and stderr from a command with log prefix +# and stdout/stderr prefix. If stdout or stderr is being piped/redirected +# it will automatically fall back to non-prefixed output. +# @arg $@ string The command to run +function stream-prefix-command-output() +{ + local stdout=stream-stdout-handler + local stderr=stream-stderr-handler + + # if stdout is being piped, print it like normal with echo + if [ ! -t 1 ]; then + # shellcheck disable=SC1007 + stdout= echo >&1 -ne + fi + + # if stderr is being piped, print it like normal with echo + if [ ! -t 2 ]; then + # shellcheck disable=SC1007 + stderr= echo >&2 -ne + fi + + "$@" > >($stdout) 2> >($stderr) +} + +# @description Print the given error message to stderr +# @arg $message string A error message. +# @stderr The error message provided with log prefix +function log-error() +{ + local msg + + if [[ $# -gt 0 ]]; then + msg="$*" + elif [[ ! -t 0 ]]; then + read -r msg || log-error-and-exit "[${FUNCNAME[0]}] could not read from stdin" + else + log-error-and-exit "[${FUNCNAME[0]}] did not receive any input arguments and STDIN is empty" + fi + + echo -e "${error_message_color}${log_prefix}ERROR -${color_clear} ${msg}" >/dev/stderr +} + +# @description Print the given error message to stderr and exit 1 +# @arg $@ string A error message. +# @stderr The error message provided with log prefix +# @exitcode 1 +function log-error-and-exit() +{ + log-error "$@" + + show-call-stack + + exit 1 +} + +# @description Print the given warning message to stderr +# @arg $@ string A warning message. +# @stderr The warning message provided with log prefix +function log-warning() +{ + local msg + + if [[ $# -gt 0 ]]; then + msg="$*" + elif [[ ! -t 0 ]]; then + read -r msg || log-error-and-exit "[${FUNCNAME[0]}] could not read from stdin" + else + log-error-and-exit "[${FUNCNAME[0]}] did not receive any input arguments and STDIN is empty" + fi + + echo -e "${warn_message_color}${log_prefix}WARNING -${color_clear} ${msg}" >/dev/stderr +} + +# @description Print the given message to stdout unless [ENTRYPOINT_QUIET_LOGS] is set +# @arg $@ string A info message. +# @stdout The info message provided with log prefix unless $ENTRYPOINT_QUIET_LOGS +function log-info() +{ + local msg + + if [[ $# -gt 0 ]]; then + msg="$*" + elif [[ ! -t 0 ]]; then + read -r msg || log-error-and-exit "[${FUNCNAME[0]}] could not read from stdin" + else + log-error-and-exit "[${FUNCNAME[0]}] did not receive any input arguments and STDIN is empty" + fi + + if [ -z "${ENTRYPOINT_QUIET_LOGS:-}" ]; then + echo -e "${notice_message_color}${log_prefix}${color_clear}${msg}" + fi +} + +# @description Print the given message to stderr unless [ENTRYPOINT_QUIET_LOGS] is set +# @arg $@ string A info message. +# @stderr The info message provided with log prefix unless $ENTRYPOINT_QUIET_LOGS +function log-info-stderr() +{ + local msg + + if [[ $# -gt 0 ]]; then + msg="$*" + elif [[ ! -t 0 ]]; then + read -r msg || log-error-and-exit "[${FUNCNAME[0]}] could not read from stdin" + else + log-error-and-exit "[${FUNCNAME[0]}] did not receive any input arguments and STDIN is empty" + fi + + if [ -z "${ENTRYPOINT_QUIET_LOGS:-}" ]; then + echo -e "${notice_message_color}${log_prefix}${color_clear}${msg}" >/dev/stderr + fi +} + +# @description Loads the dot-env files used by Docker and track the keys present in the configuration. +# @sets seen_dot_env_variables array List of config keys discovered during loading +function load-config-files() +{ + # Associative array (aka map/dictionary) holding the unique keys found in dot-env files + local -A _tmp_dot_env_keys + + for file in "${dot_env_files[@]}"; do + if ! file-exists "${file}"; then + log-warning "Could not source file [${file}]: does not exists" + continue + fi + + log-info "Sourcing ${file}" + # shellcheck disable=SC1090 + source "${file}" + + # find all keys in the dot-env file and store them in our temp associative array + for k in $(grep -v '^#' "${file}" | cut -d"=" -f1 | xargs); do + _tmp_dot_env_keys[$k]=1 + done + done + + # Used in other scripts (like templating) for [export]-ing the values + # + # shellcheck disable=SC2034 + seen_dot_env_variables=("${!_tmp_dot_env_keys[@]}") +} + +# @description Checks if $needle exists in $haystack +# @arg $1 string The needle (value) to search for +# @arg $2 array The haystack (array) to search in +# @exitcode 0 If $needle was found in $haystack +# @exitcode 1 If $needle was *NOT* found in $haystack +function in-array() +{ + local -r needle="\<${1}\>" + local -nr haystack=$2 + + [[ ${haystack[*]} =~ $needle ]] +} + +# @description Checks if $1 has executable bit set or not +# @arg $1 string The path to check +# @exitcode 0 If $1 has executable bit +# @exitcode 1 If $1 does *NOT* have executable bit +function is-executable() +{ + [[ -x "$1" ]] +} + +# @description Checks if $1 is writable or not +# @arg $1 string The path to check +# @exitcode 0 If $1 is writable +# @exitcode 1 If $1 is *NOT* writable +function is-writable() +{ + [[ -w "$1" ]] +} + +# @description Checks if $1 exists (directory or file) +# @arg $1 string The path to check +# @exitcode 0 If $1 exists +# @exitcode 1 If $1 does *NOT* exists +function path-exists() +{ + [[ -e "$1" ]] +} + +# @description Checks if $1 exists (file only) +# @arg $1 string The path to check +# @exitcode 0 If $1 exists +# @exitcode 1 If $1 does *NOT* exists +function file-exists() +{ + [[ -f "$1" ]] +} + +# @description Checks if $1 contains any files or not +# @arg $1 string The path to check +# @exitcode 0 If $1 contains files +# @exitcode 1 If $1 does *NOT* contain files +function directory-is-empty() +{ + ! path-exists "${1}" || [[ -z "$(ls -A "${1}")" ]] +} + +# @description Ensures a directory exists (via mkdir) +# @arg $1 string The path to create +# @exitcode 0 If $1 If the path exists *or* was created +# @exitcode 1 If $1 If the path does *NOT* exists and could *NOT* be created +function ensure-directory-exists() +{ + stream-prefix-command-output mkdir -pv "$@" +} + +# @description Find the relative path for a entrypoint script by removing the ENTRYPOINT_D_ROOT prefix +# @arg $1 string The path to manipulate +# @stdout The relative path to the entrypoint script +function get-entrypoint-script-name() +{ + echo "${1#"$ENTRYPOINT_D_ROOT"}" +} + +# @description Ensure a command is only run once (via a 'lock' file) in the storage directory. +# The 'lock' is only written if the passed in command ($2) successfully ran. +# @arg $1 string The name of the lock file +# @arg $@ string The command to run +function only-once() +{ + local name="${1:-$script_name}" + local file="${docker_once_path}/${name}" + shift + + if [[ -e "${file}" ]]; then + log-info "Command [${*}] has already run once before (remove file [${file}] to run it again)" + + return 0 + fi + + ensure-directory-exists "$(dirname "${file}")" + + if ! "$@"; then + return 1 + fi + + stream-prefix-command-output touch "${file}" + return 0 +} + +# @description Best effort file lock to ensure *something* is not running in multiple containers. +# The script uses "trap" to clean up after itself if the script crashes +# @arg $1 string The lock identifier +function acquire-lock() +{ + local name="${1:-$script_name}" + local file="${docker_locks_path}/${name}" + local lock_fd + + ensure-directory-exists "$(dirname "${file}")" + + exec {lock_fd}>"$file" + + log-info "🔑 Trying to acquire lock: ${file}: " + while ! ([[ -v lock_fds[$name] ]] || flock -n -x "$lock_fd"); do + log-info "🔒 Waiting on lock ${file}" + + staggered-sleep + done + + [[ -v lock_fds[$name] ]] || lock_fds[$name]=$lock_fd + + log-info "🔐 Lock acquired [${file}]" + + on-trap "release-lock ${name}" EXIT INT QUIT TERM +} + +# @description Release a lock aquired by [acquire-lock] +# @arg $1 string The lock identifier +function release-lock() +{ + local name="${1:-$script_name}" + local file="${docker_locks_path}/${name}" + + log-info "🔓 Releasing lock [${file}]" + + [[ -v lock_fds[$name] ]] || return + + # shellcheck disable=SC1083,SC2086 + flock --unlock ${lock_fds[$name]} + unset 'lock_fds[$name]' +} + +# @description Helper function to append multiple actions onto +# the bash [trap] logic +# @arg $1 string The command to run +# @arg $@ string The list of trap signals to register +function on-trap() +{ + local trap_add_cmd=$1 + shift || log-error-and-exit "${FUNCNAME[0]} usage error" + + for trap_add_name in "$@"; do + trap -- "$( + # helper fn to get existing trap command from output + # of trap -p + # + # shellcheck disable=SC2317 + extract_trap_cmd() + { + printf '%s\n' "${3:-}" + } + # print existing trap command with newline + eval "extract_trap_cmd $(trap -p "${trap_add_name}")" + # print the new trap command + printf '%s\n' "${trap_add_cmd}" + )" "${trap_add_name}" \ + || log-error-and-exit "unable to add to trap ${trap_add_name}" + done +} + +# Set the trace attribute for the above function. +# +# This is required to modify DEBUG or RETURN traps because functions don't +# inherit them unless the trace attribute is set +declare -f -t on-trap + +# @description Waits for the database to be healthy and responsive +function await-database-ready() +{ + log-info "❓ Waiting for database to be ready" + + load-config-files + + case "${DB_CONNECTION:-}" in + mysql) + # shellcheck disable=SC2154 + while ! echo "SELECT 1" | mysql --user="${DB_USERNAME}" --password="${DB_PASSWORD}" --host="${DB_HOST}" "${DB_DATABASE}" --silent >/dev/null; do + staggered-sleep + done + ;; + + pgsql) + # shellcheck disable=SC2154 + while ! echo "SELECT 1" | PGPASSWORD="${DB_PASSWORD}" psql --user="${DB_USERNAME}" --host="${DB_HOST}" "${DB_DATABASE}" >/dev/null; do + staggered-sleep + done + ;; + + sqlsrv) + log-warning "Don't know how to check if SQLServer is *truely* ready or not - so will just check if we're able to connect to it" + + # shellcheck disable=SC2154 + while ! timeout 1 bash -c "cat < /dev/null > /dev/tcp/${DB_HOST}/${DB_PORT}"; do + staggered-sleep + done + ;; + + sqlite) + log-info "${success_message_color}sqlite is always ready${color_clear}" + ;; + + *) + log-error-and-exit "Unknown database type: [${DB_CONNECTION:-}]" + ;; + esac + + log-info "${success_message_color}✅ Successfully connected to database${color_clear}" +} + +# @description sleeps between 1 and 3 seconds to ensure a bit of randomness +# in multiple scripts/containers doing work almost at the same time. +function staggered-sleep() +{ + sleep "$(get-random-number-between 1 3)" +} + +# @description Helper function to get a random number between $1 and $2 +# @arg $1 int Minimum number in the range (inclusive) +# @arg $2 int Maximum number in the range (inclusive) +function get-random-number-between() +{ + local -i from=${1:-1} + local -i to="${2:-10}" + + shuf -i "${from}-${to}" -n 1 +} + +# @description Helper function to show the bask call stack when something +# goes wrong. Is super useful when needing to debug an issue +function show-call-stack() +{ + local stack_size=${#FUNCNAME[@]} + local func + local lineno + local src + + # to avoid noise we start with 1 to skip the get_stack function + for ((i = 1; i < stack_size; i++)); do + func="${FUNCNAME[$i]}" + [ -z "$func" ] && func="MAIN" + + lineno="${BASH_LINENO[$((i - 1))]}" + src="${BASH_SOURCE[$i]}" + [ -z "$src" ] && src="non_file_source" + + log-error " at: ${func} ${src}:${lineno}" + done +} + +# @description Helper function see if $1 could be considered truthy +# returns [0] if input is truthy, otherwise [1] +# @arg $1 string The string to evaluate +# @see as-boolean +function is-true() +{ + as-boolean "${1:-}" && return 0 + + return 1 +} + +# @description Helper function see if $1 could be considered falsey +# returns [0] if input is falsey, otherwise [1] +# @arg $1 string The string to evaluate +# @see as-boolean +function is-false() +{ + as-boolean "${1:-}" && return 1 + + return 0 +} + +# @description Helper function see if $1 could be truethy or falsey. +# since this is a bash context, returning 0 is true and 1 is false +# so it works with [if is-false $input; then .... fi] +# +# This is a bit confusing, *especially* in a PHP world where [1] would be truthy and +# [0] would be falsely as return values +# @arg $1 string The string to evaluate +function as-boolean() +{ + local input="${1:-}" + local var="${input,,}" # convert input to lower-case + + case "$var" in + 1 | true) + return 0 + ;; + + 0 | false) + return 1 + ;; + + *) + log-warning "[as-boolean] variable [${var}] could not be detected as true or false, returning [1] (false) as default" + + return 1 + ;; + + esac +} diff --git a/docker/shared/root/docker/install/base.sh b/docker/shared/root/docker/install/base.sh new file mode 100755 index 000000000..a1a32a003 --- /dev/null +++ b/docker/shared/root/docker/install/base.sh @@ -0,0 +1,61 @@ +#!/bin/bash +set -ex -o errexit -o nounset -o pipefail + +# Ensure we keep apt cache around in a Docker environment +rm -f /etc/apt/apt.conf.d/docker-clean +echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache + +# Don't install recommended packages by default +echo 'APT::Install-Recommends "false";' >> /etc/apt/apt.conf + +# Don't install suggested packages by default +echo 'APT::Install-Suggests "false";' >> /etc/apt/apt.conf + +declare -a packages=() + +# Standard packages +packages+=( + apt-utils + ca-certificates + curl + git + gnupg1 + gosu + locales + locales-all + moreutils + nano + procps + software-properties-common + unzip + wget + zip +) + +# Image Optimization +packages+=( + gifsicle + jpegoptim + optipng + pngquant +) + +# Video Processing +packages+=( + ffmpeg +) + +# Database +packages+=( + mariadb-client + postgresql-client +) + +readarray -d ' ' -t -O "${#packages[@]}" packages < <(echo -n "${APT_PACKAGES_EXTRA:-}") + +apt-get update +apt-get upgrade -y +apt-get install -y "${packages[@]}" + +locale-gen +update-locale diff --git a/docker/shared/root/docker/install/php-extensions.sh b/docker/shared/root/docker/install/php-extensions.sh new file mode 100755 index 000000000..222f2374d --- /dev/null +++ b/docker/shared/root/docker/install/php-extensions.sh @@ -0,0 +1,27 @@ +#!/bin/bash +set -ex -o errexit -o nounset -o pipefail + +declare -a pecl_extensions=() + +readarray -d ' ' -t pecl_extensions < <(echo -n "${PHP_PECL_EXTENSIONS:-}") +readarray -d ' ' -t -O "${#pecl_extensions[@]}" pecl_extensions < <(echo -n "${PHP_PECL_EXTENSIONS_EXTRA:-}") + +declare -a php_extensions=() +readarray -d ' ' -t php_extensions < <(echo -n "${PHP_EXTENSIONS:-}") +readarray -d ' ' -t -O "${#php_extensions[@]}" php_extensions < <(echo -n "${PHP_EXTENSIONS_EXTRA:-}") +readarray -d ' ' -t -O "${#php_extensions[@]}" php_extensions < <(echo -n "${PHP_EXTENSIONS_DATABASE:-}") + +# Optional script folks can copy into their image to do any [docker-php-ext-configure] work before the [docker-php-ext-install] +# this can also overwirte the [gd] configure above by simply running it again +declare -r custom_pre_configure_script="" +if [[ -e "${custom_pre_configure_script}" ]]; then + if [ ! -x "${custom_pre_configure_script}" ]; then + echo >&2 "ERROR: found ${custom_pre_configure_script} but its not executable - please [chmod +x] the file!" + exit 1 + fi + + "${custom_pre_configure_script}" +fi + +# PECL + PHP extensions +IPE_KEEP_SYSPKG_CACHE=1 install-php-extensions "${pecl_extensions[@]}" "${php_extensions[@]}" diff --git a/docker/shared/root/docker/templates/shared/proxy/conf.d/docker-pixelfed.conf b/docker/shared/root/docker/templates/shared/proxy/conf.d/docker-pixelfed.conf new file mode 100644 index 000000000..0b221e604 --- /dev/null +++ b/docker/shared/root/docker/templates/shared/proxy/conf.d/docker-pixelfed.conf @@ -0,0 +1,16 @@ +########################################################### +# DO NOT CHANGE +########################################################### +# This file is generated by the Pixelfed Docker setup, and +# will be rewritten on every container start +# +# You can put any [.conf] file in this directory +# (docker-compose-state/config/proxy/conf.d) and it will +# be loaded by nginx on startup. +# +# Run [docker compose exec proxy bash -c 'nginx -t && nginx -s reload'] +# to test your config and reload the proxy +# +# See: https://github.com/nginx-proxy/nginx-proxy/blob/main/docs/README.md#custom-nginx-configuration + +client_max_body_size {{ getenv "POST_MAX_SIZE" "61M" }}; diff --git a/contrib/docker/php.production.ini b/docker/shared/root/docker/templates/usr/local/etc/php/php.ini similarity index 98% rename from contrib/docker/php.production.ini rename to docker/shared/root/docker/templates/usr/local/etc/php/php.ini index b84839ff5..130166e80 100644 --- a/contrib/docker/php.production.ini +++ b/docker/shared/root/docker/templates/usr/local/etc/php/php.ini @@ -363,7 +363,7 @@ zend.enable_gc = On ; Allows to include or exclude arguments from stack traces generated for exceptions ; Default: Off -; In production, it is recommended to turn this setting on to prohibit the output +; In production, it is recommended to turn this setting on to prohibit the output ; of sensitive information in stack traces zend.exception_ignore_args = On @@ -376,7 +376,7 @@ zend.exception_ignore_args = On ; threat in any way, but it makes it possible to determine whether you use PHP ; on your server or not. ; http://php.net/expose-php -expose_php = On +expose_php = Off ;;;;;;;;;;;;;;;;;;; ; Resource Limits ; @@ -406,7 +406,7 @@ max_input_time = 60 ; Maximum amount of memory a script may consume (128MB) ; http://php.net/memory-limit -memory_limit = 128M +memory_limit = {{ getenv "DOCKER_APP_PHP_MEMORY_LIMIT" "128M" }} ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; Error handling and logging ; @@ -462,7 +462,7 @@ memory_limit = 128M ; Development Value: E_ALL ; Production Value: E_ALL & ~E_DEPRECATED & ~E_STRICT ; http://php.net/error-reporting -error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT +error_reporting = {{ getenv "DOCKER_APP_PHP_ERROR_REPORTING" "E_ALL & ~E_DEPRECATED & ~E_STRICT" }} ; This directive controls whether or not and where PHP will output errors, ; notices and warnings too. Error output is very useful during development, but @@ -479,7 +479,7 @@ error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT ; Development Value: On ; Production Value: Off ; http://php.net/display-errors -display_errors = Off +display_errors = {{ getenv "DOCKER_APP_PHP_DISPLAY_ERRORS" "off" }} ; The display of errors which occur during PHP's startup sequence are handled ; separately from display_errors. We strongly recommend you set this to 'off' @@ -488,7 +488,7 @@ display_errors = Off ; Development Value: On ; Production Value: Off ; http://php.net/display-startup-errors -display_startup_errors = Off +display_startup_errors = {{ getenv "DOCKER_APP_PHP_DISPLAY_ERRORS" "off" }} ; Besides displaying errors, PHP can also log errors to locations such as a ; server-specific log, STDERR, or a location specified by the error_log @@ -570,8 +570,9 @@ report_memleaks = On ; Log errors to specified file. PHP's default behavior is to leave this value ; empty. ; http://php.net/error-log -; Example: -;error_log = php_errors.log +; +; NOTE: Write error log to stderr (/proc/self/fd/2 -> /dev/stderr) +error_log = /proc/self/fd/2 ; Log errors to syslog (Event Log on Windows). ;error_log = syslog @@ -679,7 +680,7 @@ auto_globals_jit = On ; Its value may be 0 to disable the limit. It is ignored if POST data reading ; is disabled through enable_post_data_reading. ; http://php.net/post-max-size -post_max_size = 64M +post_max_size = {{ getenv "POST_MAX_SIZE" "61M" }} ; Automatically add files before PHP document. ; http://php.net/auto-prepend-file @@ -831,10 +832,10 @@ file_uploads = On ; Maximum allowed size for uploaded files. ; http://php.net/upload-max-filesize -upload_max_filesize = 64M +upload_max_filesize = {{ getenv "POST_MAX_SIZE" "61M" }} ; Maximum number of files that can be uploaded via a single request -max_file_uploads = 20 +max_file_uploads = {{ getenv "MAX_ALBUM_LENGTH" "4" }} ;;;;;;;;;;;;;;;;;; ; Fopen wrappers ; @@ -947,7 +948,7 @@ cli_server.color = On [Date] ; Defines the default timezone used by the date functions ; http://php.net/date.timezone -;date.timezone = +date.timezone = {{ getenv "TZ" "UTC" }} ; http://php.net/date.default-latitude ;date.default_latitude = 31.7667 @@ -1735,7 +1736,7 @@ ldap.max_links = -1 [opcache] ; Determines if Zend OPCache is enabled -;opcache.enable=1 +opcache.enable={{ getenv "DOCKER_APP_PHP_OPCACHE_ENABLE" "1" }} ; Determines if Zend OPCache is enabled for the CLI version of PHP ;opcache.enable_cli=0 @@ -1761,12 +1762,12 @@ ldap.max_links = -1 ; When disabled, you must reset the OPcache manually or restart the ; webserver for changes to the filesystem to take effect. -;opcache.validate_timestamps=1 +opcache.validate_timestamps={{ getenv "DOCKER_APP_PHP_OPCACHE_VALIDATE_TIMESTAMPS" "0" }} ; How often (in seconds) to check file timestamps for changes to the shared ; memory storage allocation. ("1" means validate once per second, but only ; once per request. "0" means always validate) -;opcache.revalidate_freq=2 +opcache.revalidate_freq={{ getenv "DOCKER_APP_PHP_OPCACHE_REVALIDATE_FREQ" "2" }} ; Enables or disables file search in include_path optimization ;opcache.revalidate_path=0 diff --git a/docker/shared/root/shared/proxy/conf.d/.gitignore b/docker/shared/root/shared/proxy/conf.d/.gitignore new file mode 100644 index 000000000..e69de29bb diff --git a/docker/shell b/docker/shell new file mode 100755 index 000000000..7b725e1b0 --- /dev/null +++ b/docker/shell @@ -0,0 +1,17 @@ +#!/bin/bash + +declare service="${PF_SERVICE:=worker}" +declare user="${PF_USER:=www-data}" + +declare -a command=("bash") + +if [[ $# -ge 1 ]]; then + command=("$@") +fi + +exec docker compose exec \ + --user "${user}" \ + --env TERM \ + --env COLORTERM \ + "${service}" \ + "${command[@]}" diff --git a/goss.yaml b/goss.yaml new file mode 100644 index 000000000..73f245c64 --- /dev/null +++ b/goss.yaml @@ -0,0 +1,123 @@ +# See: https://github.com/goss-org/goss/blob/master/docs/manual.md#goss-manual + +package: + curl: { installed: true } + ffmpeg: { installed: true } + gifsicle: { installed: true } + gosu: { installed: true } + jpegoptim: { installed: true } + locales-all: { installed: true } + locales: { installed: true } + mariadb-client: { installed: true } + nano: { installed: true } + optipng: { installed: true } + pngquant: { installed: true } + postgresql-client: { installed: true } + unzip: { installed: true } + wget: { installed: true } + zip: { installed: true } + +user: + www-data: + exists: true + uid: 33 + gid: 33 + groups: + - www-data + home: /var/www + shell: /usr/sbin/nologin + +command: + php-version: + exit-status: 0 + exec: 'php -v' + stdout: + - PHP {{ .Env.EXPECTED_PHP_VERSION }} + stderr: [] + + php-extensions: + exit-status: 0 + exec: 'php -m' + stdout: + - bcmath + - Core + - ctype + - curl + - date + - dom + - exif + - fileinfo + - filter + - gd + - hash + - iconv + - imagick + - intl + - json + - libxml + - mbstring + - mysqlnd + - openssl + - pcntl + - pcre + - PDO + - pdo_mysql + - pdo_pgsql + - pdo_sqlite + - Phar + - posix + - readline + - redis + - Reflection + - session + - SimpleXML + - sodium + - SPL + - sqlite3 + - standard + - tokenizer + - xml + - xmlreader + - xmlwriter + - zip + - zlib + stderr: [] + + forego-version: + exit-status: 0 + exec: 'forego version' + stdout: + - dev + stderr: [] + + gomplate-version: + exit-status: 0 + exec: 'gomplate -v' + stdout: + - gomplate version + stderr: [] + + gosu-version: + exit-status: 0 + exec: 'gosu -v' + stdout: + - '1.12' + stderr: [] + +{{ if eq .Env.PHP_BASE_TYPE "nginx" }} + nginx-version: + exit-status: 0 + exec: 'nginx -v' + stdout: [] + stderr: + - 'nginx version: nginx' +{{ end }} + +{{ if eq .Env.PHP_BASE_TYPE "apache" }} + apache-version: + exit-status: 0 + exec: 'apachectl -v' + stdout: + - 'Server version: Apache/' + stderr: [] +{{ end }} diff --git a/tests/bats/helpers.bats b/tests/bats/helpers.bats new file mode 100644 index 000000000..6ba65c2c3 --- /dev/null +++ b/tests/bats/helpers.bats @@ -0,0 +1,103 @@ +setup() { + DIR="$(cd "$(dirname "${BATS_TEST_FILENAME:-}")" >/dev/null 2>&1 && pwd)" + ROOT="$(dirname "$(dirname "$DIR")")" + + load "$ROOT/docker/shared/root/docker/helpers.sh" +} + +teardown() { + if [[ -e test_dir ]]; then + rm -rf test_dir + fi +} + +@test "test [is-true]" { + is-true "1" + is-true "true" + is-true "TrUe" +} + +@test "test [is-false]" { + is-false "0" + is-false "false" + is-false "FaLsE" +} + +@test "test [is-false-expressions-0]" { + if is-false "0"; then + return 0 + fi + + return 1 +} + +@test "test [is-false-expressions-false]" { + if is-false "false"; then + return 0 + fi + + return 1 +} + +@test "test [is-false-expressions-FaLse]" { + if is-false "FaLse"; then + return 0 + fi + + return 1 +} + +@test "test [is-false-expressions-invalid]" { + if is-false "invalid"; then + return 0 + fi + + return 1 +} + +@test "test [is-true-expressions-1]" { + if is-true "1"; then + return 0 + fi + + return 1 +} + +@test "test [is-true-expressions-true]" { + if is-true "true"; then + return 0 + fi + + return 1 +} + +@test "test [is-true-expressions-TrUE]" { + if is-true "TrUE"; then + return 0 + fi + + return 1 +} + +@test "test [directory-is-empty] - non existing" { + directory-is-empty test_dir +} + +@test "test [directory-is-empty] - actually empty" { + mkdir -p test_dir + + directory-is-empty test_dir +} + +@test "test [directory-is-empty] - not empty (directory)" { + mkdir -p test_dir/sub-dir + + ! directory-is-empty test_dir +} + +@test "test [directory-is-empty] - not empty (file)" { + mkdir -p test_dir/ + touch test_dir/hello-world.txt + + ! directory-is-empty test_dir +}