Masterplan Optimiser

Server Installation

The Masterplan Optimiser server hosts the web calendar that organisers access on their phones. It runs as a set of Docker containers behind a Caddy reverse proxy with automatic HTTPS. This guide walks you through provisioning a VPS, deploying the application, and creating the first admin account.

Who is this for?

This guide is intended for NC Board Members, coordinators, or technical volunteers who want to provide the web calendar for Head-Organisers in their network. You do not need this if you only use the desktop application.

Requirements

RequirementDetails
VPSUbuntu 22.04 or 24.04 (1 vCPU, 1 GB RAM minimum). Providers like Hetzner, DigitalOcean, or Linode all work.
Domain nameA domain (e.g. mp-opt.net) pointing to the VPS IP address via an A record.
SSH accessRoot SSH access to the VPS for the initial setup.
GitThe repository must be cloned on the server.

Step 1 - Provision the VPS

  1. Create a new VPS with Ubuntu 22.04 or 24.04 at your chosen provider.
  2. Note the public IP address of the VPS.
  3. In your domain registrar's DNS settings, create an A record pointing your domain to the VPS IP address.
    Type: A
    Name: @ (or your subdomain)
    Value: <your-vps-ip>
    TTL: 300
  4. Wait for DNS propagation (usually a few minutes to an hour).

Step 2 - Run the Server Setup Script

SSH into your VPS as root and run the setup script. This installs Docker, configures the firewall, and creates a deploy user.

ssh root@<your-vps-ip>

# Download and run the setup script
git clone https://github.com/Brian-Funk/MasterplanOptimiserV3---Server.git /opt/masterplan
cd /opt/masterplan
sudo bash deploy/setup-server.sh

The setup script performs the following:

  • Updates the system and installs essential packages (curl, git, ufw, fail2ban)
  • Configures the firewall (UFW) to allow SSH (22), HTTP (80), and HTTPS (443)
  • Installs Docker and the Docker Compose plugin
  • Creates a deploy user with Docker permissions
  • Sets up the application directory at /opt/masterplan

Step 3 - Generate the Production Configuration

Run the interactive configuration script. It will prompt you for your domain and generate all required environment variables.

# On the VPS (as deploy user or root)
cd /opt/masterplan
bash configure-production.sh

The script will ask for:

PromptExampleNotes
Domainmp-opt.netRequired. Must match your DNS.
Database password(auto-generated)Press Enter to auto-generate a secure password.
WebAuthn RP nameGC CalendarDisplay name shown during passkey registration. Default is fine.

This generates a .env file containing all configuration:

DATABASE_URL=postgresql://masterplan:<password>@db:5432/masterplan
POSTGRES_PASSWORD=<auto-generated>
SECRET_KEY=<auto-generated>
CORS_ORIGINS=["https://mp-opt.net"]
WEBAUTHN_RP_ID=mp-opt.net
WEBAUTHN_RP_NAME=GC Calendar
WEBAUTHN_ORIGIN=https://mp-opt.net
COOKIE_SECURE=true
DOMAIN=mp-opt.net
SESSION_TTL_HOURS=8
SESSION_TTL_HOURS_ADMIN=1

Important: Keep the .env file safe and do not commit it to version control. It contains database credentials and secret keys. For additional security, you can move sensitive values into Docker Secrets (see Step 4b below).

Step 4 - Deploy the Application

Run the deployment script. This builds the frontend, creates the Docker containers, and starts everything.

cd /opt/masterplan
bash deploy/deploy.sh

Step 4b - Docker Secrets (Optional)

By default, all secrets live in the .env file. For additional security, you can move SECRET_KEY and VAPID_PRIVATE_KEY into Docker Secrets. Docker Secrets mount as files inside the container at /run/secrets/ and are not visible via docker inspect.

# Create the secrets directory
mkdir -p secrets

# Extract secrets from .env into individual files
grep -oP '^SECRET_KEY=\\K.*' .env > secrets/secret_key
grep -oP '^VAPID_PRIVATE_KEY=\\K.*' .env > secrets/vapid_private_key

# Lock down permissions
chmod 700 secrets
chmod 600 secrets/*

Once the secret files are in place, you can remove SECRET_KEY and VAPID_PRIVATE_KEY from your .env file. The application resolves secrets through a priority chain: Docker Secrets files first, then environment variables.

Note: POSTGRES_PASSWORD must remain in .env because Docker Compose uses it to build the DATABASE_URL connection string before containers start.

Redeploy to activate Docker Secrets:

bash deploy/deploy.sh

Verify the secrets are being loaded from the files by checking the startup logs:

docker logs <backend-container> 2>&1 | head -10
# Should show:
# [Secrets] 'SECRET_KEY' resolved from Docker secret file
# [Secrets] 'VAPID_PRIVATE_KEY' resolved from Docker secret file

The deployment script performs these steps:

  1. Pulls the latest code from the repository
  2. Builds the Next.js frontend as a static export (to web/out/)
  3. Builds and starts the Docker containers:
    • PostgreSQL 16 - database
    • FastAPI backend - REST API on port 8000 (internal)
    • Caddy - reverse proxy with automatic HTTPS via Let's Encrypt (ports 80 and 443)
  4. Runs a health check to verify the server is running

Step 5 - Register the Root Admin

  1. Open your browser and navigate to https://your-domain.com/bootstrap.
  2. The bootstrap page lets you create the first admin account using a passkey (biometric or security key). This page is only available when no admin account exists yet.
  3. Enter a display name and register your passkey.
  4. Once registered, you are logged in as the root admin. You can now manage events, users, and settings from the admin panel.
The bootstrap page showing the root admin registration form with a passkey prompt
The bootstrap page for registering the first admin account.

Server Architecture

ContainerPortRole
Caddy80, 443 (public)Reverse proxy, automatic HTTPS (Let's Encrypt), serves static frontend, security headers
Backend (FastAPI)8000 (internal)REST API, authentication (passkeys), session management, WebAuthn
PostgreSQL 165432 (internal)Database for users, events, tasks, and sessions

Caddy handles all HTTPS certificates automatically - no manual certificate management is needed. The frontend is served as static files from /srv/static, and API requests to /api/* are proxied to the backend container.

Browser / Phone
HTTPS (443)
Docker
Caddy- Ports 80, 443 (public)

Reverse proxy, automatic HTTPS (Let's Encrypt), serves static frontend, security headers

/api/*
Static files
Backend (FastAPI)

Port 8000 (internal)

  • REST API
  • WebAuthn (Passkeys)
  • Session Management
PostgreSQL 16

Port 5432 (internal)

  • Users, Events, Tasks
  • Sessions & Audit Log
TCP

Server architecture: Caddy routes HTTPS traffic to the backend and serves the static frontend.

Updating the Server

To deploy a new version, simply re-run the deploy script:

cd /opt/masterplan
bash deploy/deploy.sh

This pulls the latest code, rebuilds the frontend, and restarts the containers with zero-downtime for the database (the PostgreSQL volume is preserved).

Optional - Enable Web Push Notifications

If you want organisers to receive push notifications (e.g. for schedule changes or announcements), add VAPID keys to your .env file:

# Generate VAPID keys (run once on any machine with Python)
python -c "from py_vapid import Vapid; v = Vapid(); v.generate_keys(); print('Private:', v.private_pem()); print('Public:', v.public_key)"

# Add to .env
VAPID_PRIVATE_KEY=<base64url-private-key>
VAPID_CLAIMS_EMAIL=mailto:your-email@example.com

After adding the keys, redeploy with bash deploy/deploy.sh. If you have set up Docker Secrets (Step 4b), you can also place the VAPID private key into secrets/vapid_private_key and remove it from .env.

Troubleshooting

HTTPS certificate not issued

Make sure your DNS A record is correctly pointing to the VPS IP and has propagated. Caddy needs to reach ports 80 and 443 to obtain a certificate from Let's Encrypt. Check the Caddy logs:

docker compose -f infra/docker-compose.yml logs caddy

Health check fails

Check that all containers are running:

docker compose -f infra/docker-compose.yml -f infra/docker-compose.prod.yml ps

Check the backend logs for errors:

docker compose -f infra/docker-compose.yml logs backend

Bootstrap page not accessible

The /bootstrap route is only available when no admin account exists. If you have already registered an admin, log in normally and manage users from the admin panel.

Database connection error

Ensure the POSTGRES_PASSWORD in your .env matches the password in DATABASE_URL. If you changed the password, you may need to recreate the database volume:

docker compose -f infra/docker-compose.yml down -v
bash deploy/deploy.sh

Next Steps