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.

On a shared cPanel account instead?

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

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

  1. SSH in as root

    From your local terminal:

    ssh root@YOUR_DROPLET_IP
  2. Update the system

    apt update && apt upgrade -y
  3. Create a non-root sudo user

    Running everything as root long-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 deploy for the rest of this guide (ssh deploy@YOUR_DROPLET_IP), prefixing commands with sudo where needed.

  4. 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
No Composer or Node.js needed on the server

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:

FieldValue
HostYour Droplet's IP address, or your domain once DNS resolves
ProtocolSFTP — SSH File Transfer Protocol
Port22
Usernamedeploy (the user you created in step 3)
AuthenticationYour 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
No wrapping folder, and dotfiles are easy to miss

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
Why AllowOverride All matters

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
Deployed the code as a different user (e.g. root via git/rsync)?

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
Every minute, not once a day

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.

  1. Install Supervisor

    sudo apt install -y supervisor
  2. 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
  3. Start it

    sudo supervisorctl reread
    sudo supervisorctl update
    sudo supervisorctl start moneymate-worker:*

    Check it's running with sudo supervisorctl status — you should see moneymate-worker:moneymate-worker_00 as RUNNING.

Simpler fallback: skip the worker entirely

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/syslog after a few minutes.
  • If you set up the queue worker, watch storage/logs/worker.log while 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 mysqldump cron) before real customer data accumulates.