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=your@email.com
SMTP_PASSWORD=your_smtp_password
SMTP_FROM=noreply@yourdomain.com
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": "admin@mydaycare.com",
"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": "admin@mydaycare.com",
"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 dumpmedia_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_DIRpoints to the correct directory - Verify Certbot has write access and the domain resolves to your server
- Check nginx logs:
docker compose logs nginx