Explorar o código

Merge pull request #4844 from jippi/jippi-fork

Refactor Docker/Compose
daniel hai 1 ano
pai
achega
0bd3e0ab80
Modificáronse 53 ficheiros con 3702 adicións e 792 borrados
  1. 1 1
      .ddev/commands/redis/redis-cli
  2. 30 8
      .dockerignore
  3. 18 0
      .editorconfig
  4. 1276 147
      .env.docker
  5. 0 78
      .env.example
  6. 4 2
      .env.testing
  7. 0 125
      .github/workflows/build-docker.yml
  8. 231 0
      .github/workflows/docker.yml
  9. 25 17
      .gitignore
  10. 5 0
      .hadolint.yaml
  11. 4 0
      .markdownlint.json
  12. 12 0
      .shellcheckrc
  13. 14 0
      .vscode/extensions.json
  14. 21 0
      .vscode/settings.json
  15. 18 0
      CODEOWNERS
  16. 307 0
      Dockerfile
  17. 20 19
      app/Console/Kernel.php
  18. 1 1
      config/filesystems.php
  19. 0 35
      contrib/docker-nginx.conf
  20. 0 100
      contrib/docker/Dockerfile.apache
  21. 0 90
      contrib/docker/Dockerfile.fpm
  22. 0 15
      contrib/docker/start.apache.sh
  23. 0 15
      contrib/docker/start.fpm.sh
  24. 0 67
      contrib/nginx.conf
  25. 42 0
      docker-compose.migrate.yml
  26. 193 57
      docker-compose.yml
  27. 5 0
      docker/README.md
  28. 8 0
      docker/apache/root/etc/apache2/conf-available/remoteip.conf
  29. 11 0
      docker/artisan
  30. 45 0
      docker/dottie
  31. 0 0
      docker/fpm/root/.gitkeep
  32. 2 0
      docker/nginx/Procfile
  33. 49 0
      docker/nginx/root/docker/templates/etc/nginx/conf.d/default.conf
  34. 41 0
      docker/nginx/root/docker/templates/etc/nginx/nginx.conf
  35. 31 0
      docker/shared/root/docker/entrypoint.d/01-permissions.sh
  36. 21 0
      docker/shared/root/docker/entrypoint.d/02-check-config.sh
  37. 33 0
      docker/shared/root/docker/entrypoint.d/04-defaults.envsh
  38. 60 0
      docker/shared/root/docker/entrypoint.d/05-templating.sh
  39. 13 0
      docker/shared/root/docker/entrypoint.d/10-storage.sh
  40. 38 0
      docker/shared/root/docker/entrypoint.d/11-first-time-setup.sh
  41. 42 0
      docker/shared/root/docker/entrypoint.d/12-migrations.sh
  42. 9 0
      docker/shared/root/docker/entrypoint.d/20-horizon.sh
  43. 11 0
      docker/shared/root/docker/entrypoint.d/30-cache.sh
  44. 105 0
      docker/shared/root/docker/entrypoint.sh
  45. 593 0
      docker/shared/root/docker/helpers.sh
  46. 61 0
      docker/shared/root/docker/install/base.sh
  47. 27 0
      docker/shared/root/docker/install/php-extensions.sh
  48. 16 0
      docker/shared/root/docker/templates/shared/proxy/conf.d/docker-pixelfed.conf
  49. 16 15
      docker/shared/root/docker/templates/usr/local/etc/php/php.ini
  50. 0 0
      docker/shared/root/shared/proxy/conf.d/.gitignore
  51. 17 0
      docker/shell
  52. 123 0
      goss.yaml
  53. 103 0
      tests/bats/helpers.bats

+ 1 - 1
.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 "$@"

+ 30 - 8
.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

+ 18 - 0
.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

+ 1276 - 147
.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"
-
-OPEN_REGISTRATION=true
-ENFORCE_EMAIL_VERIFICATION=false
-PF_MAX_USERS=1000
-OAUTH_ENABLED=true
-
-APP_TIMEZONE=UTC
-APP_LOCALE=en
-
-## 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
-
-## Instance
-#INSTANCE_DESCRIPTION=
-INSTANCE_PUBLIC_HASHTAGS=false
-#INSTANCE_CONTACT_EMAIL=
-INSTANCE_PUBLIC_LOCAL_TIMELINE=false
-#BANNED_USERNAMES=
-STORIES_ENABLED=false
-RESTRICTED_INSTANCE=false
-
-## 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
-
-## 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
-
-## Databases (Postgres)
-#DB_CONNECTION=pgsql
-#DB_HOST=postgres
-#DB_PORT=5432
-#DB_DATABASE=pixelfed
-#DB_USERNAME=postgres
-#DB_PASSWORD=postgres
-
-## Cache (Redis)
-REDIS_CLIENT=phpredis
-REDIS_SCHEME=tcp
-REDIS_HOST=redis
-REDIS_PASSWORD=redis_password
-REDIS_PORT=6379
-REDIS_DATABASE=0
-
-HORIZON_PREFIX="horizon-"
-
-## EXPERIMENTS 
-EXP_LC=false
-EXP_REC=false
-EXP_LOOPS=false
-
-## 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
-
-## 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
-
-## Horizon
-HORIZON_DARKMODE=false
-
-## COSTAR - Confirm Object Sentiment Transform and Reduce
-PF_COSTAR_ENABLED=false
-
-# Media
-MEDIA_EXIF_DATABASE=false
-
-## Logging
-LOG_CHANNEL=stderr
-
-## Image
-IMAGE_DRIVER=imagick
-
-## Broadcasting: log driver for local development
-BROADCAST_DRIVER=log
-
-## Cache
-CACHE_DRIVER=redis
-
-## Purify
-RESTRICT_HTML_TYPES=true
-
-## Queue
-QUEUE_DRIVER=redis
-
-## Session
-SESSION_DRIVER=redis
-
-## Trusted Proxy
-TRUST_PROXIES="*"
-
-## Passport
-#PASSPORT_PRIVATE_KEY=
-#PASSPORT_PUBLIC_KEY=
+# Prefix for container names (without any dash at the end)
+# @dottie/validate required
+DOCKER_ALL_CONTAINER_NAME_PREFIX="${APP_DOMAIN}"
+
+# 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"
+
+# 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"
+
+# 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"
+
+# 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"
+
+# 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"
+
+# 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}"
+
+################################################################################
+# docker app
+################################################################################
+
+# The docker tag prefix to use for pulling images, can be one of
+#
+#  * latest
+#  * <some semver release>
+#  * staging
+#  * edge
+#  * branch-<some branch name>
+#  * pr-<some merge request id>
+#
+# 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"
+
+# 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"
+
+# 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"
+
+# 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"
+
+# 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"
+
+# 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"
+
+# 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}"
+
+# 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"
+
+# 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"
+
+# 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"
+
+# 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=""
+
+# 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"
+
+# Show the "diff" when applying templating to files
+#
+# @default "1"
+# @dottie/validate required,boolean
+#DOCKER_APP_ENTRYPOINT_SHOW_TEMPLATE_DIFF="1"
+
+# Docker entrypoints that should be skipped on startup
+# @default ""
+#ENTRYPOINT_SKIP_SCRIPTS=""
+
+# 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=""
+
+# 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=""
+
+# 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"

+ 0 - 78
.env.example

@@ -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=<BucketName>
-#AWS_URL=
-#AWS_ENDPOINT=
-#AWS_USE_PATH_STYLE_ENDPOINT=false

+ 4 - 2
.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

+ 0 - 125
.github/workflows/build-docker.yml

@@ -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

+ 231 - 0
.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/<user>/<project>/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/<user>/<project>/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/<user>/<project>/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/<user>/<project>/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 }}

+ 25 - 17
.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

+ 5 - 0
.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 <package>` use `apt-get install <package>=<version>`
+  - SC2046 # warning: Quote this to prevent word splitting.
+  - SC2086 # info: Double quote to prevent globbing and word splitting.

+ 4 - 0
.markdownlint.json

@@ -0,0 +1,4 @@
+{
+    "MD013": false,
+    "MD014": false
+}

+ 12 - 0
.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

+ 14 - 0
.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"
+    ]
+}

+ 21 - 0
.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"
+    }
+}

+ 18 - 0
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

+ 307 - 0
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"]

+ 20 - 19
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');
     }

+ 1 - 1
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),

+ 0 - 35
contrib/docker-nginx.conf

@@ -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;
-}

+ 0 - 100
contrib/docker/Dockerfile.apache

@@ -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"]

+ 0 - 90
contrib/docker/Dockerfile.fpm

@@ -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"]

+ 0 - 15
contrib/docker/start.apache.sh

@@ -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

+ 0 - 15
contrib/docker/start.fpm.sh

@@ -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

+ 0 - 67
contrib/nginx.conf

@@ -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;
-}

+ 42 - 0
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:

+ 193 - 57
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
+    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
-    env_file:
-      - .env.docker
+    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:
-      - app-storage:/var/www/storage
-      - app-bootstrap:/var/www/bootstrap
-    networks:
-      - external
-      - internal
-    command: gosu www-data php artisan horizon
+      - "./.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

+ 5 - 0
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.

+ 8 - 0
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

+ 11 - 0
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 "${@}"

+ 45 - 0
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}" "$@"

+ 0 - 0
docker/fpm/root/.gitkeep


+ 2 - 0
docker/nginx/Procfile

@@ -0,0 +1,2 @@
+fpm: php-fpm
+nginx: nginx -g "daemon off;"

+ 49 - 0
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;
+    }
+}

+ 41 - 0
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;
+}

+ 31 - 0
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

+ 21 - 0
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

+ 33 - 0
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

+ 60 - 0
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 /<path>)
+: "${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

+ 13 - 0
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

+ 38 - 0
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

+ 42 - 0
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

+ 9 - 0
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

+ 11 - 0
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

+ 105 - 0
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 "$@"

+ 593 - 0
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
+}

+ 61 - 0
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

+ 27 - 0
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[@]}"

+ 16 - 0
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" }};

+ 16 - 15
contrib/docker/php.production.ini → 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

+ 0 - 0
docker/shared/root/shared/proxy/conf.d/.gitignore


+ 17 - 0
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[@]}"

+ 123 - 0
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 }}

+ 103 - 0
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
+}