Installation
Set up Event Schedule on your own server with this step-by-step guide. For automated installation, consider using Softaculous or Docker.
Overview
This guide walks you through manually installing Event Schedule on your own server. There are five steps: create an empty MySQL database, extract the release files, set file permissions, run the browser-based setup wizard, and add the cron job. The wizard writes your configuration to .env and creates the database tables for you, so there is nothing to import by hand.
These steps describe a plain selfhosted install, where you own every schedule on the server. If you want to run a multi-tenant service where other people sign up and get their own subdomain and plan, follow the SaaS setup guide instead.
For easier installation, you can use:
- Softaculous - One-click installation on cPanel hosts
- Docker - Containerized deployment with Docker Compose
A selfhosted install is not a reduced edition. It resolves to the Enterprise feature set, so ticketing, check-in, custom fields, event graphics, webhooks, custom CSS, AI features and unlimited newsletters are all available with no plan to buy. A few controls only make sense on the hosted service (per-schedule email settings, subscription billing) and are hidden here.
Requirements
Before you begin, ensure your server meets the following requirements:
| Requirement | Minimum Version | Notes |
|---|---|---|
| PHP | 8.2+ | With required extensions (see below) |
| MySQL | 5.7+ or MariaDB 10.3+ | The only supported database; the setup wizard configures the MySQL connection |
| Web Server | Apache or Nginx | With mod_rewrite or equivalent, and its document root on the public directory |
| SSL Certificate | Required | Links are generated as https:// outside local environments, and session cookies are secure-only by default |
| Cron | Every minute | Runs scheduled tasks and the queue worker (step 5); email and calendar sync stop without it |
Required PHP Extensions
- BCMath
- Ctype
- Fileinfo
- Intl
- JSON
- Mbstring
- OpenSSL
- PDO (with MySQL driver)
- Tokenizer
- XML
- cURL
- GD
- MySQLi - used by the Test button in the setup wizard, which checks your credentials before migrations run
- Zip - used by backup export and import, and by
php artisan app:update
Image work (thumbnails, social images, event graphics) is done with GD, and generating an event graphic fails outright if GD is missing. Imagick is not used anywhere, so installing it is not a substitute.
1. Set Up the Database
Create a MySQL database and user for Event Schedule. Run the following commands in your MySQL client:
CREATE DATABASE eventschedule;
CREATE USER 'eventschedule'@'localhost' IDENTIFIED BY 'change_me';
GRANT ALL PRIVILEGES ON eventschedule.* TO 'eventschedule'@'localhost';
Leave the database completely empty. There is no schema to import: the setup wizard in step 4 runs the migrations and creates every table. The user needs full privileges on that database, because migrations create, alter and index tables.
Replace change_me with a strong, unique password. Never use default or weak passwords in production. The setup wizard also requires a password, so a user with a blank password will not be accepted.
2. Download the Application
Download the latest release and extract it into the directory that will hold the install.
- Download eventschedule.zip from the latest GitHub release
- Upload the zip file to your server
- Extract the contents to your web root directory
# Example: Extract to web root
cd /var/www
unzip eventschedule.zip
The archive has no wrapping folder inside it: app, public, storage and the rest land directly in whatever directory you unzip into. So cd into the directory you want the install to live in before extracting.
The release zip is built with dependencies already installed and the frontend assets already compiled, so you do not run composer install or npm run build after extracting. Those are only needed if you install from a git clone instead.
Your web server should point to the public directory inside the install directory, not the install directory itself. Getting this wrong is the single most common cause of a broken install, and its symptoms are described under troubleshooting.
3. Set File Permissions
Ensure the web server has proper permissions to write to storage and cache directories.
cd /path/to/eventschedule
chmod -R 755 storage
sudo chown -R www-data:www-data storage bootstrap public
All three directories are needed: storage holds logs, uploads and caches, bootstrap holds the compiled config and route caches, and public has to be writable because setup creates the public/storage symlink that serves uploaded images.
The user www-data is typical for Apache on Debian/Ubuntu. Your web server may run under a different user (e.g., nginx, apache, or http). Check your server configuration. Docker images based on Alpine often have no www-data name at all, only the numeric UID 82, so use chown -R 82:82 ... there instead.
The web-server user must also be able to write to the .env file, not just storage. The app writes its APP_KEY there on the first request, and the setup wizard saves your database configuration to the same file, so include .env in the ownership change above (e.g. sudo chown www-data:www-data .env). If it is read-only, the wizard shows a warning at the top of the form and stops before touching the database rather than leaving you half configured.
4. Configure Environment
Copy the example environment file to create your configuration:
cp .env.example .env
Do not set APP_URL in .env yourself. The setup wizard appears while it is blank (or if the database has no tables), and it writes the correct value for you once setup succeeds. You also do not need to fill in the database credentials by hand; just create the empty database from step 1 and enter its details in the wizard, which creates all the tables.
Now access your application at https://your-domain.com in your browser. Because APP_URL is still blank, every request is redirected to the setup wizard, which is the sign-up page. Work through it in order:
- Enter the database connection: MySQL Host, Port, Database, Username and Password, pre-filled from the
DB_*values in your.env. All five are required, so a MySQL user with a blank password is not accepted. - Press Test. The account fields below stay hidden until the connection succeeds, so this is not an optional check. If the database already contains an Event Schedule installation, Test says so and keeps the form disabled, which is what stops you overwriting an existing site.
- Create the admin account: Email, Full Name and a password of at least 8 characters. This first account becomes the instance admin.
- Accept the selfhosting terms, and optionally tick Report errors to the developers to help us improve the app, which sets
REPORT_ERRORS=trueso crashes are sent to the developers. - Press Sign Up. The wizard runs the migrations first, and only writes to
.envonce they succeed, so a bad database never leaves you with a half-configured install. It then setsAPP_URLto the address you loaded the wizard on, setsAPP_ENV=production, saves theDB_*values, and creates thepublic/storagesymlink.
Nothing on this screen sets up mail. MAIL_MAILER ships as log, which writes messages to storage/logs/laravel.log and delivers nothing, so ticket confirmations and verification emails will silently go nowhere until you edit the MAIL_* values yourself. See Email Setup.
Everything else is configured by editing .env directly. If you ever run php artisan config:cache, re-run it (or php artisan config:clear) after each change, or the old values stay live.
User Accounts and Registration
A selfhosted install is single user by default. The first account you create in the setup wizard becomes the instance admin, and after that the sign-up page is closed: visiting it sends you to the login page instead.
Most people never need to change this. Your audience does not need accounts to use the site: they can buy tickets, RSVP, leave post-event feedback, and submit fan photos, videos and comments as guests.
If you do want other people to be able to register on your server, enable it in .env:
ALLOW_REGISTRATION=true
There is no separate attendee-only role: a registered user can create their own schedules and events on your server. Only turn this on for a server you control access to, such as one on a private network or behind an authenticating proxy. If you want to host separate, independent tenants, run in SaaS mode with IS_HOSTED=true instead, which gives each schedule its own plan and settings.
HTTPS and Session Cookies
Once APP_ENV is production, which is what the wizard writes, every generated link uses https://, and SESSION_SECURE_COOKIE ships as true so the session cookie is only sent over HTTPS. On a server reached over plain HTTP that combination looks like a broken login: the sign-in form accepts your password and returns you to the login page, because the browser never stored the session. Install a certificate, or for a local test install only, set SESSION_SECURE_COOKIE=false.
Running Behind a Reverse Proxy
If Event Schedule sits behind a reverse proxy or CDN (Nginx, Apache, Cloudflare, or a control panel such as HestiaCP), tell it which proxies to trust so it reads the X-Forwarded-Proto and X-Forwarded-For headers those proxies set:
TRUSTED_PROXIES=*
Use * to trust any proxy, or a comma-separated list of proxy IPs or CIDR ranges (for example 10.0.0.0/8,192.168.1.1) when the origin server is reachable directly from the internet.
Without this setting the application treats every request as plain HTTP even when the browser is on HTTPS, which can produce redirect loops, and it records the proxy's IP address as the visitor's IP in analytics and rate limiting.
5. Set Up the Cron Job
This step is not optional. One cron entry drives everything that happens on a timer rather than because somebody clicked: scheduled newsletters, reminder and feedback emails, calendar sync, and releasing unpaid ticket reservations back into stock. Email sent during a page request, such as a ticket confirmation, still goes out without it, so an install missing this line looks perfectly healthy while every timed job silently never runs.
Add the following line to your server's crontab:
* * * * * php /path/to/eventschedule/artisan schedule:run
Adding the Cron Job
To edit your crontab, run:
crontab -e
Add the cron line at the end of the file, making sure to replace /path/to/eventschedule with your actual installation path.
If using cPanel, you can add cron jobs via the "Cron Jobs" section in your control panel without using the command line.
What the Scheduler Runs
A single minutely cron entry is enough because the scheduler decides internally what is due. The main jobs:
| Task | How often |
|---|---|
Queue worker, which drains queued email, push and webhook jobs. It has nothing to do on the shipped QUEUE_CONNECTION=sync setting, where that work runs inside the web request instead |
Every minute |
| Send scheduled newsletters | Every minute |
| Retry failed jobs | Every 5 minutes |
| Google, Outlook and CalDAV calendar sync, AI translation | Every 15 minutes |
| Release unpaid ticket reservations, expire waitlist offers, send feedback requests, appointment and carpool reminders, event graphic emails | Hourly |
| Renew calendar webhooks, prune old logs and backups, notify owners about new booking requests, fan content and poll options, run curator auto-imports | Daily |
| Refresh the GeoIP database used for visitor-location analytics | Monthly |
Run php /path/to/eventschedule/artisan schedule:run by hand first. It prints the tasks it ran, or "No scheduled commands are ready to run", and any error it prints is what cron would have hit silently. To then prove cron itself is firing, append your own redirect to the crontab line (>> /path/to/cron.log 2>&1) and check that file a couple of minutes later. If it stays empty, cron is not running the command: make sure the php in your crontab is the same binary the site uses, and that the path to artisan is absolute.
Verification
After completing the installation, verify everything is working correctly:
- Access the application: Visit
https://your-domain.comand confirm the homepage loads - Sign in: Log in with the admin account you created in the wizard. There is no second registration step: sign-up is closed once that account exists, unless you set
ALLOW_REGISTRATION=true - Create a schedule: Create a test schedule and add an event, then open its public page to confirm images load
- Check the cron job: Run
php artisan schedule:runonce by hand and confirm it completes without an error, then check the redirect file from step 5 to confirm cron is calling it too - Check logs: Review
storage/logs/laravel.logfor any errors
If you encounter any issues during installation, check the GitHub Issues or start a Discussion.
Next Steps
Now that Event Schedule is installed, you may want to:
- Configure email delivery, which nothing else works properly without
- Configure Stripe payments for ticket sales
- Add a Gemini or OpenAI key to turn on AI event import, agenda scanning and translation
- Set up Google Calendar integration
- Set up Outlook Calendar integration
- Set up Twilio to text invitations to venues or talent you added by phone number, and to send WhatsApp messages (
TWILIO_SID,TWILIO_AUTH_TOKEN,TWILIO_FROM_NUMBER) - Enable push notifications with OneSignal (optional)
- Add a Turnstile challenge to your public forms (optional)
- Turn on federation to share your public events with the eventschedule.com listings (optional, off by default)
- Tour the admin panel, where you can watch the queue, read logs, edit translations and change platform settings
Upgrades are one step, from either direction: open Settings > App Update, which shows your installed version next to the latest release and offers an Update button when they differ, or run php artisan app:update on the server. Both download and install the new release and then run any new migrations. Take a backup first. Your uploads, custom translations and anything else under storage/app/ are excluded from the update by design, so they survive it. Instance admins get the same panel at Admin > System > App Update, which also badges the System menu when a release is waiting.
If the App Update screen is not there at all, use the command: it works on every install and does not depend on the screen. That is also the way back from an older release whose UI hid the update button.
Push Notifications (Optional)
Event Schedule can send browser and mobile web push notifications alongside the emails it already sends, using OneSignal. The same moments trigger both: a ticket sale, a booking request accepted or declined, new feedback, a waitlist opening, a finished backup export or import. Push is off by default: if you do not configure it, no push SDK is loaded and your installation makes no calls to OneSignal.
To enable it, create a free OneSignal app (Web platform), then set these values in your .env:
ONESIGNAL_APP_ID=your-onesignal-app-id
ONESIGNAL_REST_API_KEY=your-onesignal-rest-api-key
Both values are needed; with only one set, push stays off. There is also an optional ONESIGNAL_SAFARI_WEB_ID, which you only need for legacy macOS Safari web push.
Enabling OneSignal loads its SDK from OneSignal's CDN and sends notification data to OneSignal's servers. Visitors choose to opt in per device; nothing is sent until they allow notifications.
Once configured, a schedule's settings gain a Push notifications panel on the Notifications tab, with Enable push on this device and, after that, Send test push. Opting in is per device, so each browser you want alerts on has to be enabled separately. Apple iOS only supports web push for sites the visitor adds to their home screen (iOS 16.4+); Android and desktop browsers work without installation.
Spam Protection (Optional)
Event Schedule can put a Cloudflare Turnstile challenge in front of every form a stranger can reach. Turnstile is invisible to most visitors and needs no puzzle-solving.
Once configured, the challenge is added to:
- Sign in, sign up and password reset
- Ticket checkout and gift card purchases
- RSVPs and appointment bookings
- Events submitted by guests through a schedule's submission page
- Fan photo, video and comment submissions
Create a free Turnstile widget for your domain, then set both values in your .env:
TURNSTILE_SITE_KEY=your-turnstile-site-key
TURNSTILE_SECRET_KEY=your-turnstile-secret-key
If either one is missing the challenge is skipped entirely, so a half-filled configuration leaves those forms unprotected without any warning. Enabling Turnstile loads Cloudflare's widget script on the affected pages.
Turnstile is deliberately inactive on tenant custom domains, because a site key is registered against specific hostnames and would fail to validate on a domain you do not control. If you run a multi-tenant SaaS with custom domains, expect those pages to fall back to no challenge.
Troubleshooting
"Permission denied" writing storage/logs/laravel.log
The web-server user cannot write to storage (and often .env). Re-run the ownership and permission commands from the file permissions step, making sure to use the user your web server actually runs as. On Alpine-based Docker images that is the numeric UID 82, not www-data.
A 500 error, or you can't get back to the setup wizard
This usually means the database has no tables yet (migrations did not run). The setup wizard reappears automatically whenever the database is empty, so fix the underlying cause (database privileges or the file permissions above) and reload the page to run setup again.
If the wizard still does not appear, open .env, clear the APP_URL value so it is blank, and reload. The wizard will run again and rewrite APP_URL once setup succeeds.
No email is ever delivered
Start with MAIL_MAILER, which ships as log. That writes the whole message into storage/logs/laravel.log instead of sending it, so finding your "missing" emails in that file confirms the diagnosis. Set your real mail credentials as described in Email Setup.
If only the timed messages are missing (reminders, feedback requests, scheduled newsletters) while ticket confirmations arrive normally, the mail configuration is fine and the cron job is not running. If you have switched QUEUE_CONNECTION away from the shipped sync value, a growing jobs table is the same symptom: nothing is draining the queue.
Uploaded images do not load
Uploads are served through a public/storage symlink that setup creates for you. If public was not writable at the time, setup carries on without it and images 404 afterwards. Fix the ownership from the file permissions step, then create the link yourself with php artisan storage:link.
Ticket QR codes don't scan, or the site answers on /public
Your web server's document root must point at the public directory inside the project, not at the project folder itself. When the root is set one level too high, the app answers on both https://your-domain.com/ and https://your-domain.com/public/, and links generated from a page you reached through /public/ carry that segment too.
The give-away is that the same ticket shows one QR code when opened from a confirmation email and a different one when opened from the Sales page. Fix the document root, then confirm that APP_URL in .env exactly matches the address people use to reach the site. APP_URL is what the app trusts when it builds the URL inside a ticket's QR code, so it must include a sub-path if you genuinely serve the app from one.
QR codes printed or emailed before you correct APP_URL keep pointing at the old address. Re-send the ticket email to reissue them.
Custom translations
Rename built-in UI terms (for example "Talent" to "Artist", or "Curator" to "Event Planner") without your changes being wiped out by php artisan app:update.
The easiest way to customize translations is the Translations page in the admin panel (System > Translations): search every string, edit any language, and optionally share improvements with the community. Hand-made files described below keep working and are adopted into the editor automatically.
Drop a PHP file in:
storage/app/lang/{locale}/{file}.php
The admin editor manages three files: messages.php (UI strings), accessibility.php, and marketing.php. List the keys you want to change and nothing else; the bundled translations fill in the rest:
<?php
// storage/app/lang/en/messages.php
return [
'talent' => 'Artist',
'talents' => 'Artists',
'curator' => 'Event Planner',
'curators' => 'Event Planners',
];
Create one directory per locale you want to override (en, es, fr, …). The full list of supported locales lives in config/app.php under supported_languages.
Files for any other group, such as validation.php or auth.php, are honored the same way. They simply sit outside the admin editor, which never rewrites or prunes them.
Changes apply on the next request, no cache clear is required. storage/app/ is gitignored, so your overrides survive php artisan app:update, git pull, and fresh checkouts. New keys added in future releases continue to show their bundled English (or translated) value until you override them.
Custom dashboard links
Add up to three custom links to the sidebar of the admin portal (for example a support site, community forum, or internal tool). They appear for everyone signed in, just below the Newsletters link, and open in a new tab. This works in both selfhosted and SaaS deployments.
Set the following variables in your .env file. A link only appears when both its title and URL are filled in, so you can configure one, two, or three links:
CUSTOM_LINK_1_TITLE="Support"
CUSTOM_LINK_1_URL="https://support.example.com"
CUSTOM_LINK_2_TITLE="Community"
CUSTOM_LINK_2_URL="https://community.example.com"
CUSTOM_LINK_3_TITLE=
CUSTOM_LINK_3_URL=
If you have run php artisan config:cache, re-run it (or php artisan config:clear) after editing .env so the new links take effect.