A web-based photo gallery and event management system built with PHP and MySQL/MariaDB. This project allows users to register, log in, upload and view images, manage events, and participate in leaderboards. It also includes an admin interface and email notifications using PHPMailer.
- User Registration & Authentication: Secure user registration, login, e-mail verification and password reset.
- Photo Gallery: Guests upload photos from their phone – also several at once, with progress – and view them in the gallery and live slideshow.
- Event Management: Admins can create and manage events.
- Leaderboard: Track and display top users or event participants.
- Admin Panel: Manage users, events, and gallery content.
- Email Notifications: Uses PHPMailer for sending emails (e.g., verification, notifications).
- Download as ZIP: Download all images of an event (plus a CSV export) as a ZIP archive.
- Configurable via Docker: Includes Docker and Docker Compose setup for easy deployment.
├── Dockerfile # Multi-stage build (Composer deps + PHP/Apache runtime)
├── docker-compose.yml # Production-style stack (app + MariaDB, Traefik labels)
├── composer.json / .lock # PHP dependencies (PHPMailer, QR code generator, PHPStan, PHPUnit)
├── tests/ # PHPUnit tests
│ └── e2e/ # Playwright end-to-end tests (own package.json)
├── docker/
│ ├── apache.conf # Security headers, blocks lib/ and script execution in uploads/
│ ├── php.ini # Upload limits, session hardening, OPcache
│ └── entrypoint.sh # Runs DB migrations, then starts Apache
├── db/migrations/ # Versioned schema migrations (NNNN_name.sql / .php)
└── src/ # Web root
├── lib/ # Internal includes – never served over HTTP
│ ├── auth.php # Sessions, login throttling, access checks
│ ├── config.php # Configuration from environment variables
│ ├── db.php # Database connection
│ ├── helpers.php # Escaping, CSRF, UUIDs, safe image uploads
│ ├── images.php # Thumbnail / display-size variants
│ ├── mail.php # E-mails via PHPMailer
│ ├── metadata.php # Lossless removal of GPS/location data from photos
│ ├── migrate.php # CLI migration runner
│ └── migrations.php # Migration logic + bootstrap admin
├── index.php # Guest upload page (per event)
├── admin.php # Dashboard
├── manage_event.php # Event settings, drinks, invites
├── manage_guests.php # Block devices / remove spam
├── gallery.php, slideshow.php, leaderboard.php
├── image.php # Serves (and lazily creates) resized images
└── healthz.php # Container health check
- Docker with Docker Compose
Updating an existing installation? See UPGRADING.md for the steps per release.
- Copy
example.envto.envand fill in real values (DB passwords, SMTP,APP_URL, admin user). - Start the stack:
The provided
docker compose up -d
docker-compose.ymlexpects an externaltraefiknetwork. For a local test without Traefik, addports: ["8080:8080"]to thepicdropservice and open http://localhost:8080. The container listens on port 8080 (it runs without root).
| Variable | Description |
|---|---|
APP_URL |
Public base URL, e.g. https://picdrop.example.com. Used for e-mail links and the QR code. |
PAGE_TITLE |
Name shown in the browser title and e-mails. |
REGISTRATION_CODE |
Code required for open sign-ups. Empty = only invited users can register. |
ADMIN_USERNAME / ADMIN_PASSWORD / ADMIN_EMAIL |
Creates an admin account on first start if all three are set. |
DB_HOST / DB_USER / DB_PASS / DB_NAME |
Database connection. |
SMTP_* |
Mail server settings (see example.env). SMTP_SECURE is tls, ssl or none. |
SKIP_MIGRATIONS=1 |
Don't run migrations on container start. |
- MariaDB 12.3 LTS (supported until June 2029).
MARIADB_AUTO_UPGRADEupgrades the data directory automatically when the image version changes (tested from 10.11). - Always take a backup before changing the database version:
docker compose exec db sh -c 'mariadb-dump -uroot -p"$MYSQL_ROOT_PASSWORD" --single-transaction --all-databases' > backup.sql
- Migrations in
db/migrationsare applied automatically when the container starts (php src/lib/migrate.php), guarded by a DB lock so parallel starts are safe. - New change? Add the next file, e.g.
db/migrations/0005_add_something.sql. Migrations must be idempotent (IF NOT EXISTS, …) because MariaDB can't roll back DDL. For data migrations use a.phpfile that returnsfunction (mysqli $conn): void { … }.
composer install # dependencies incl. PHPStan
composer lint # php -l on all files
composer analyse # PHPStan
composer test # PHPUnit
composer migrate # apply migrations against DB_* from the environmentPlaywright tests run in real browsers (desktop Chrome and an iPhone for the guest page) against the
production docker-compose.yml plus a test override with Mailpit
catching all e-mails:
docker build -t picdrop:e2e .
cd tests/e2e
npm ci && npx playwright install chromium webkit
npm run stack:up # start the stack (http://localhost:18080, Mailpit UI: http://localhost:18025)
npm test # run the tests; `npx playwright test --ui` for the interactive mode
npm run report # HTML report with screenshots/traces of failures
npm run stack:down.github/workflows/ci.yml(pull requests): Composer validate/audit, PHP lint, PHPStan, PHPUnit, migration test against MariaDB, Hadolint, a Docker build, checks of the container hardening and the Playwright end-to-end tests (report with screenshots/traces is uploaded on failure)..github/workflows/release.yml(push tomain): runs CI, then semantic-release (version + changelog) and publishes a multi-arch image (linux/amd64,linux/arm64) toghcr.io/<owner>/<repo>. Image tags:latest,vX.Y.Z,vX.YandvXpoint to releases;mainandsha-<commit>are built from every push tomainand may contain unreleased changes. Every image carries an SBOM and SLSA build provenance, and the provenance is signed keylessly via GitHub/Sigstore. Verify an image before deploying:gh attestation verify oci://ghcr.io/greidal/picdrop:v0.4.0 --owner Greidal docker buildx imagetools inspect ghcr.io/greidal/picdrop:v0.4.0 --format '{{json .SBOM}}'- Dependabot keeps Composer packages, Docker images and GitHub Actions up to date (weekly). MariaDB major/minor upgrades are excluded on purpose — upgrade between LTS versions deliberately.
- Use strong, unique values for all passwords and the registration code; never commit
.env. - Uploaded files are validated by content, stored under random names and can't be executed.
- All state-changing forms are CSRF-protected; logins are throttled per account.
- Password reset links are single-use, expire after 60 minutes and are stored hashed; verification links expire after 7 days. Account mails are limited to 3 per address and hour, and the forms don't reveal whether an account exists.
- Optional per event: GPS/location data is removed from uploaded photos (lossless; JPEG, PNG, WebP).
- Apache/PHP run as
www-data(UID 33) on port 8080; application code is owned by root and read-only. - No setuid/setgid binaries; PHP has shell functions and remote file access disabled and is
restricted to
/var/wwwand/tmp(open_basedir). docker-compose.ymlruns the app with a read-only root filesystem (tmpfs for/tmp), all Linux capabilities dropped,no-new-privileges, PID/memory limits and rotated logs.- The database only sits on an internal network without internet access and keeps just the capabilities its entrypoint needs.
Contributions are welcome – see CONTRIBUTING.md for the workflow and AGENTS.md for conventions and security rules (also read by AI coding agents). Security issues: please report them privately as described in SECURITY.md.
Brought to you by Klimarschanlage Vertrieb Ltd. Contact our team via mail for licensing information, help or to thank them for their incredible work.
- PHPMailer for email functionality.