Skip to content

Deployment Guide

This guide walks you through deploying Minispace on your own server from scratch.

Step 1 — Clone the Repository

git clone https://github.com/minispace-app/minispace.git
cd minispace

Step 2 — Configure Environment Variables

Copy the production environment template:

cp .env.prod.example .env

Open .env and fill in every value. The sections below explain each one.

Database

POSTGRES_USER=garderie
POSTGRES_PASSWORD=your_secure_db_password   # change this
POSTGRES_DB=minispace

JWT Secrets

Generate two strong random secrets (minimum 32 characters each):

openssl rand -base64 48   # run twice, use each output for one secret
JWT_SECRET=your_long_random_secret_here
JWT_REFRESH_SECRET=your_long_random_refresh_secret_here
JWT_EXPIRY_SECONDS=900        # access token lifetime (15 min)
JWT_REFRESH_EXPIRY_DAYS=30    # refresh token lifetime

Encryption Master Key

All uploaded files are encrypted at rest using AES-256-GCM. Generate a 32-byte key:

openssl rand -hex 32
ENCRYPTION_MASTER_KEY=your_64_character_hex_string_here

Critical: back up this key

If you lose the ENCRYPTION_MASTER_KEY, all uploaded files become permanently unreadable. Store it securely (password manager, secrets vault, etc.) and never commit it to version control.

SMTP Email

Minispace sends emails for 2FA codes, invitations, notifications, and journals. You need an SMTP server.

SMTP_HOST=smtp.gmail.com         # or your SMTP provider
SMTP_PORT=587
SMTP_USERNAME=[email protected]
SMTP_PASSWORD=your_smtp_password
SMTP_FROM=[email protected]

Gmail users

Use an App Password rather than your Google account password. Alternatively, services like Mailgun, Resend, or Postmark work well.

Super Admin Key

This key protects the super-admin API used to create and manage daycares.

SUPER_ADMIN_KEY=replace_with_a_strong_random_string

Generate one with:

openssl rand -base64 32

App URLs

APP_BASE_URL=https://yourdomain.com
NEXT_PUBLIC_API_URL=/api
NEXT_PUBLIC_WS_URL=/ws

Nginx & SSL

NGINX_ENV=prod
NGINX_HTTP_PORT=80
NGINX_HTTPS_PORT=443
SSL_CERTS_DIR=/etc/letsencrypt

Backups (optional)

BACKUP_ENCRYPT_PASSWORD=your_backup_password   # optional, encrypts backup files

Step 3 — SSL Certificate

Get a certificate with Certbot:

# Install Certbot
sudo apt install certbot

# Obtain certificate (stop nginx first if port 80 is in use)
sudo certbot certonly --standalone -d yourdomain.com

The certificates will be at /etc/letsencrypt/live/yourdomain.com/.

Note

The production nginx config expects certificates at: - /etc/nginx/ssl/live/minispace.app/fullchain.pem - /etc/nginx/ssl/live/minispace.app/privkey.pem

Update nginx/nginx.prod.conf to replace minispace.app with your own domain before deploying.

Step 4 — Update the Nginx Config for Your Domain

Open nginx/nginx.prod.conf and replace all occurrences of minispace.app with your domain:

sed -i 's/minispace\.app/yourdomain.com/g' nginx/nginx.prod.conf

Not using Cloudflare?

The production nginx config blocks all traffic that does not come through Cloudflare IPs. If you are not using Cloudflare, remove the geo $cloudflare_ip block and the associated if ($cloudflare_ip = 0) checks from nginx.prod.conf, or use the development config (nginx.dev.conf) as your starting point.

Step 5 — Pull and Start the Services

Pull the pre-built images from GitHub Container Registry and start the stack:

docker compose -f docker-compose.prod.yml pull
docker compose -f docker-compose.prod.yml up -d

Check that all services are running:

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

All services should show running. View logs if anything is wrong:

docker compose -f docker-compose.prod.yml logs -f api

Step 6 — Create Your First Daycare

Minispace uses a super-admin API to provision daycares. Use the SUPER_ADMIN_KEY you set in .env:

curl -X POST https://yourdomain.com/api/super-admin/garderies \
  -H "X-Super-Admin-Key: your_super_admin_key" \
  -H "Content-Type: application/json" \
  -d '{
    "slug": "my-daycare",
    "name": "My Daycare",
    "email": "[email protected]",
    "phone": "514-555-0100",
    "address": "123 Main St, Montreal, QC"
  }'

This provisions a new PostgreSQL schema for the daycare. Users can then access it at:

https://yourdomain.com

with their slug subdomain (if you've set up wildcard DNS) or via the X-Tenant header.

Step 7 — Create the First Admin User

After creating a daycare, add the first admin user:

curl -X POST https://yourdomain.com/api/super-admin/garderies/my-daycare/users \
  -H "X-Super-Admin-Key: your_super_admin_key" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "[email protected]",
    "password": "temp_password",
    "first_name": "Admin",
    "last_name": "User",
    "role": "admin_garderie"
  }'

The admin can then log in at https://yourdomain.com and change their password from their profile.

Backups

The backup service runs automatically every night at 02:00 and keeps 30 days of backups in ./backups/.

To run a manual backup immediately:

docker compose -f docker-compose.prod.yml run --rm \
  -e MANUAL_BACKUP=true backup

Backup files are stored in ./backups/ on the host:

  • db_TIMESTAMP.sql.gz — PostgreSQL dump
  • media_TIMESTAMP.tar.gz — all uploaded files

Restore from Backup

# Restore database
gunzip -c backups/db_2026-02-01T02:00:00.sql.gz | \
  docker exec -i minispace_db psql -U garderie minispace

# Restore media files
docker run --rm -v minispace_media_files:/data/media \
  -v $(pwd)/backups:/backup alpine \
  tar -xzf /backup/media_2026-02-01T02:00:00.tar.gz -C /data/media

Updating Minispace

To update to a newer version:

# Pull new images
docker compose -f docker-compose.prod.yml pull

# Restart services (migrations run automatically on startup)
docker compose -f docker-compose.prod.yml up -d

Database migrations are applied automatically when the API container starts.

Environment Variables Reference

Variable Required Description
POSTGRES_USER Yes PostgreSQL username
POSTGRES_PASSWORD Yes PostgreSQL password
POSTGRES_DB Yes PostgreSQL database name
JWT_SECRET Yes JWT signing secret (min 32 chars)
JWT_REFRESH_SECRET Yes JWT refresh token secret (min 32 chars)
JWT_EXPIRY_SECONDS No Access token TTL, default 900
JWT_REFRESH_EXPIRY_DAYS No Refresh token TTL, default 30
ENCRYPTION_MASTER_KEY Yes 64-char hex key for file encryption
SMTP_HOST Yes SMTP server hostname
SMTP_PORT Yes SMTP port (usually 587)
SMTP_USERNAME Yes SMTP username
SMTP_PASSWORD Yes SMTP password
SMTP_FROM Yes Sender email address
SUPER_ADMIN_KEY Yes Secret key for super-admin API
APP_BASE_URL Yes Public base URL of your deployment
NEXT_PUBLIC_API_URL Yes Frontend API URL (usually /api)
NEXT_PUBLIC_WS_URL Yes Frontend WebSocket URL (usually /ws)
NGINX_ENV No dev or prod, default dev
SSL_CERTS_DIR No Path to SSL certs, default /etc/letsencrypt
BACKUP_ENCRYPT_PASSWORD No Password to encrypt backup archives
FCM_API_KEY No Firebase Cloud Messaging key (push notifications)
RUST_LOG No Log level, default info

Troubleshooting

API container fails to start

Check the logs:

docker compose -f docker-compose.prod.yml logs api

Common causes: - DATABASE_URL is wrong or the database isn't reachable - ENCRYPTION_MASTER_KEY is missing or not exactly 64 hex characters - JWT_SECRET is too short (minimum 32 characters)

Emails are not being sent

  • Verify SMTP_* values are correct
  • Check that port 587 is not blocked by your server's firewall
  • Look for SMTP errors in the API logs: docker compose logs api | grep -i smtp

SSL certificate issues

  • Make sure SSL_CERTS_DIR points to the correct directory
  • Verify Certbot has write access and the domain resolves to your server
  • Check nginx logs: docker compose logs nginx