Getting Started
VPS Hosting (DigitalOcean & Apache)
A full root-access deployment on a DigitalOcean Droplet running Apache — provisioning the server from a blank Ubuntu image through to a live, SSL-secured, cron-and-queue-ready installation.
This page assumes root SSH access to a fresh server — you're provisioning the whole stack yourself. If your host gives you a cPanel control panel instead of a terminal, use cPanel Hosting & Cron instead — same destination, entirely different route.
- 1. Create the Droplet
- 2. Point your domain at it
- 3. First login & basic hardening
- 4. Install Apache, PHP & MySQL
- 5. Create the database
- 6. Upload & extract the application
- 7. Configure the Apache virtual host
- 8. File permissions
- 9. Enable SSL with Certbot
- 10. Run the installer
- 11. Set up the cron job
- 12. Queue worker with Supervisor
- After launch
1. Create the Droplet
From the DigitalOcean control panel, click Create → Droplets and configure:
Image
Ubuntu 24.04 (LTS) x64. Any recent Ubuntu LTS works fine — this guide's package names assume Ubuntu/Debian's apt.
Size
The Basic plan, Regular SSD, 1 vCPU / 1–2GB RAM is comfortable for MoneyMate plus MySQL and Apache on a single box for small-to-medium traffic. Size up later with a few clicks if you need to — DigitalOcean resizes are non-destructive.
Authentication
Choose SSH Key over a root password. Paste your local machine's public key (~/.ssh/id_ed25519.pub or similar) — DigitalOcean installs it for you, no separate step needed after boot.
Hostname
Anything memorable — e.g. moneymate-prod. Purely cosmetic, doesn't need to match your domain.
Click Create Droplet and note the public IPv4 address once it boots (usually under a minute).
2. Point your domain at it
In your domain registrar or DNS provider, create an A record for your domain (and www, if you want both) pointing at the Droplet's IP address:
Type Name Value TTL A @ 143.198.xxx.xxx 3600 A www 143.198.xxx.xxx 3600
DNS propagation is usually quick but can take up to a few hours depending on your registrar. You can start the server setup below immediately — you don't need to wait for DNS to resolve until the SSL step.
3. First login & basic hardening
SSH in as root
From your local terminal:
ssh root@YOUR_DROPLET_IP
Update the system
apt update && apt upgrade -y
Create a non-root sudo user
Running everything as
rootlong-term is a bad habit — create a dedicated user for day-to-day work:adduser deploy usermod -aG sudo deploy
Log out and back in as
deployfor the rest of this guide (ssh deploy@YOUR_DROPLET_IP), prefixing commands withsudowhere needed.Enable the firewall
Allow only what's needed — SSH and web traffic:
sudo ufw allow OpenSSH sudo ufw allow "WWW Full" sudo ufw enable
"WWW Full"is Apache's UFW application profile covering both port 80 (HTTP) and 443 (HTTPS) — it registers itself automatically once Apache is installed in the next step, so this command only needs to run after that.
4. Install Apache, PHP & MySQL
Ubuntu's default repositories lag behind the PHP version MoneyMate needs, so add Ondřej Surý's well-known PHP PPA first:
sudo apt install -y software-properties-common sudo add-apt-repository -y ppa:ondrej/php sudo apt update
Then install Apache, PHP 8.3, every extension the installer's requirements check verifies, plus GD (avatars/image handling) and ZIP (report/data exports):
sudo apt install -y apache2 mysql-server \ php8.3 libapache2-mod-php8.3 php8.3-cli \ php8.3-mysql php8.3-mbstring php8.3-xml php8.3-curl \ php8.3-bcmath php8.3-gd php8.3-zip php8.3-intl unzip
The zip archive you upload already ships with vendor/ (production PHP dependencies) and a pre-built public/build/ (compiled frontend assets). There's nothing to composer install or npm run build on the server itself — just PHP, Apache, and MySQL to run what's already there.
Enable the two Apache modules the application relies on and restart:
sudo a2enmod rewrite sudo systemctl restart apache2
Finally, secure the fresh MySQL installation (sets a root password, removes anonymous users and the test database):
sudo mysql_secure_installation
5. Create the database
Log into MySQL as root and create a dedicated database and user for the application:
sudo mysql -u root -p
CREATE DATABASE moneymate CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE USER 'moneymate'@'localhost' IDENTIFIED BY 'a-strong-password-here'; GRANT ALL PRIVILEGES ON moneymate.* TO 'moneymate'@'localhost'; FLUSH PRIVILEGES; EXIT;
Keep the database name, username, and password handy — you'll enter them into the installer's Database step shortly.
6. Upload & extract the application
The zip archive is roughly 40–45MB. Any of the three methods below gets it onto the server — pick whichever you're most comfortable with.
Option A — scp (simplest, one command)
From your local machine (not the server), copy the zip archive up over SSH:
scp moneymate.zip deploy@YOUR_DROPLET_IP:/home/deploy/
This reuses the same SSH key you set up when creating the Droplet — no separate password or configuration needed. Watch for it to report 100% before moving on.
Option B — rsync (resumable, better for slow connections)
If your upload is large relative to your connection or you want to retry a dropped transfer without starting over:
rsync -avz --progress moneymate.zip deploy@YOUR_DROPLET_IP:/home/deploy/
Re-running the exact same command after an interruption picks up where it left off instead of re-uploading from scratch.
Option C — SFTP with a GUI client
If you'd rather drag-and-drop with FileZilla, Cyberduck, or Transmit, connect using:
| Field | Value |
|---|---|
| Host | Your Droplet's IP address, or your domain once DNS resolves |
| Protocol | SFTP — SSH File Transfer Protocol |
| Port | 22 |
| Username | deploy (the user you created in step 3) |
| Authentication | Your private SSH key — the same one used to log in via ssh, not a password |
Upload the single zip file into /home/deploy/, then extract it on the server via SSH as below — dragging the ~12,000 individual extracted files through an SFTP client instead would take dramatically longer than uploading one archive and unzipping it server-side.
Extract it
Back on the server (over SSH), first make sure there's enough disk space and the archive isn't corrupted from the transfer:
df -h /var/www unzip -t /home/deploy/moneymate.zip | tail -1
unzip -t tests every file in the archive without extracting anything — it should end with No errors detected in compressed data. Then extract into /var/www, the standard location for web applications on a Debian/Ubuntu box:
sudo mkdir -p /var/www/moneymate sudo unzip -q /home/deploy/moneymate.zip -d /var/www/moneymate sudo chown -R www-data:www-data /var/www/moneymate
The archive's top level has no wrapping folder — app/, artisan, composer.json, .env.example, and .htaccess extract directly into whatever destination folder you give unzip -d, exactly as the commands above expect. .env.example and .htaccess both start with a dot, and ls hides those by default — confirm they actually made it over with ls -la /var/www/moneymate before continuing (plain ls won't show them).
Once you've confirmed the extraction, delete the zip to reclaim the disk space:
rm /home/deploy/moneymate.zip
7. Configure the Apache virtual host
Create a new site configuration pointing straight at the project's public folder — the correct, clean document root, no .htaccess redirect trick required:
sudo nano /etc/apache2/sites-available/moneymate.conf
<VirtualHost *:80>
ServerName yourdomain.com
ServerAlias www.yourdomain.com
DocumentRoot /var/www/moneymate/public
<Directory /var/www/moneymate/public>
Options -Indexes +FollowSymLinks
AllowOverride All
Require all granted
</Directory>
ErrorLog ${APACHE_LOG_DIR}/moneymate-error.log
CustomLog ${APACHE_LOG_DIR}/moneymate-access.log combined
</VirtualHost>
Enable the site and disable Apache's default placeholder page:
sudo a2ensite moneymate.conf sudo a2dissite 000-default.conf sudo systemctl reload apache2
Laravel's front controller relies on the .htaccess file inside public/ to rewrite clean URLs to index.php. Without AllowOverride All, Apache ignores that file entirely and every route except the homepage 404s.
8. File permissions
Laravel needs write access to two folders. Since the application already runs as www-data (set in step 6), this is usually already correct — but confirm and lock down the mode explicitly:
sudo chown -R www-data:www-data /var/www/moneymate/storage /var/www/moneymate/bootstrap/cache sudo chmod -R 775 /var/www/moneymate/storage /var/www/moneymate/bootstrap/cache
The web installer's last few steps also need to create the public/storage symlink — that's a one-time write into public/ itself, not storage/, so it's not covered by the command above. If you deployed as root or another non-www-data user, public/ stays owned by that user and the installer's own attempt fails silently (the install still completes, but receipt/avatar uploads won't display). Either chown -R www-data:www-data /var/www/moneymate/public before running the installer, or just run php artisan storage:link yourself once from the project root afterward — an admin notice tells you if this step needs it.
9. Enable SSL with Certbot
Free, auto-renewing Let's Encrypt certificates via Certbot's Apache plugin — requires your DNS A record (step 2) to already be resolving to this server:
sudo apt install -y certbot python3-certbot-apache sudo certbot --apache -d yourdomain.com -d www.yourdomain.com
Certbot edits your virtual host to redirect HTTP → HTTPS automatically and installs a systemd timer that renews the certificate before it expires — nothing further to do. Confirm the timer is active:
sudo systemctl status certbot.timer
10. Run the installer
Visit your domain in a browser and follow the Installation & Setup wizard, using the database credentials from step 5.
11. Set up the cron job (required)
MoneyMate's scheduled tasks — daily budget-threshold checks, due-date reminders, recurring-transaction processing, net worth snapshots, the weekly digest email, and package grace-period sweeps — all run through Laravel's scheduler, which needs exactly one cron entry to drive it. Edit the crontab for the www-data user (the same user the application runs as):
sudo crontab -u www-data -e
Add this line, running every minute:
* * * * * php /var/www/moneymate/artisan schedule:run >> /dev/null 2>&1
The cron entry itself runs every minute — that's normal and expected. Laravel's scheduler checks on each invocation whether any of the tasks in routes/console.php are actually due, and only runs the ones that are (most are configured ->daily() or ->weeklyOn()). Running the cron entry less often than every minute means those tasks can fire late or be silently skipped.
12. Queue worker with Supervisor (recommended)
Notification emails (budget alerts, due reminders, the weekly digest, AI-action completion) are queued jobs, not sent inline, so a real request never waits on an outgoing email. Unlike shared cPanel hosting, a VPS can run a genuine persistent php artisan queue:work process — Supervisor keeps it alive and restarts it automatically if it ever crashes.
Install Supervisor
sudo apt install -y supervisor
Create a worker config
sudo nano /etc/supervisor/conf.d/moneymate-worker.conf
[program:moneymate-worker] process_name=%(program_name)s_%(process_num)02d command=php /var/www/moneymate/artisan queue:work --sleep=3 --tries=3 --max-time=3600 autostart=true autorestart=true stopasgroup=true killasgroup=true user=www-data numprocs=1 redirect_stderr=true stdout_logfile=/var/www/moneymate/storage/logs/worker.log stopwaitsecs=3600
Start it
sudo supervisorctl reread sudo supervisorctl update sudo supervisorctl start moneymate-worker:*
Check it's running with
sudo supervisorctl status— you should seemoneymate-worker:moneymate-worker_00asRUNNING.
If you'd rather not run a background process at all, set QUEUE_CONNECTION=sync in /var/www/moneymate/.env instead (then php artisan config:clear). Every queued job then runs immediately, inline, the moment it's dispatched — no worker to manage, at the cost of the triggering request waiting slightly longer whenever an email is sent.
After launch
- Confirm the cron ran by checking Site Settings → Activity Log or
/var/log/syslogafter a few minutes. - If you set up the queue worker, watch
storage/logs/worker.logwhile sending a test email to confirm it's actually processing jobs. - Send yourself a test budget-threshold alert, or use the "Send test email" action in SMTP Settings, to confirm mail delivery end-to-end.
- If Stripe is configured, add your webhook endpoint (
https://yourdomain.com/stripe/webhook) in the Stripe Dashboard so subscription events reach the application. - Consider enabling DigitalOcean's automated Droplet backups (or your own
mysqldumpcron) before real customer data accumulates.