Deploy on a VM with Docker Compose
One Linux server runs everything: Caddy for automatic HTTPS, the backend and panel, Postgres with pgvector, and Redis. 1 vCPU and 1 GB of RAM with 2 GB of swap is enough for internal use.
1Point DNS at the server#
Create A records for api.<domain> and app.<domain> pointing at the server's public IP. On Cloudflare, keep them DNS only (grey cloud) so Caddy can get the certificates itself.
2Prepare the server#
curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker $USER && newgrp docker
# 2 GB swap for small machines
sudo fallocate -l 2G /swapfile && sudo chmod 600 /swapfile && sudo mkswap /swapfile && sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
# firewall + automatic security updates
sudo apt-get update && sudo apt-get install -y ufw unattended-upgrades git
sudo ufw allow OpenSSH && sudo ufw allow 80,443/tcp && sudo ufw --force enablePostgres and Redis are never exposed; only ports 22, 80 and 443 are open.
3Get the code and configure#
git clone git@github.com:Mangosteen-Studio/mango-agent.git /opt/mango-agent
cd /opt/mango-agent/deploy
cp env.production.example .env && nano .env # domains, secrets, Firebase, R2, RedisFor a private repository, give the server a read-only GitHub deploy key.
4Build and start#
docker compose -f docker-compose.prod.yml --env-file .env --profile local-redis up -d --build
# using Upstash instead? set REDIS_URL=rediss://… and drop --profile local-redis
curl https://api.<domain>/ready # {"status":"ready"}On start the backend applies pending migrations, then serves the API and runs the worker. Caddy requests certificates on the first request. On a 1 GB machine the panel build is slow; you can build both images on your laptop, push them to a registry, and set BACKEND_IMAGE and PANEL_IMAGE in .env.
5Create the first owner#
cp -r seed.example seed && nano seed/org.json # your email as owner
docker compose -f docker-compose.prod.yml run --rm -T -v "$PWD/seed:/seed:ro" backend \
uv run python -m app.seeds.load /seed6Backups and updates#
backup.sh writes a gzip'd pg_dump to deploy/backups/ (the last 14 are kept) and, when BACKUP_S3_BUCKET is set, uploads it to an off-site R2 bucket. A lost server or disk then doesn't take the backups with it. Use a separate mango-backups bucket with a token scoped to it only.
./backup.sh # dump + "uploaded db/mango-….sql.gz"
( crontab -l; echo "15 3 * * * cd /opt/mango-agent/deploy && ./backup.sh >> backups/backup.log 2>&1" ) | crontab -
./restore.sh r2:latest # restore the newest off-site dump (asks you to type "restore")
# update
cd /opt/mango-agent && git pull && cd deploy && docker compose -f docker-compose.prod.yml --env-file .env --profile local-redis up -d --buildIf the server is lost, create a new VM, repeat the steps above with the same .env, then run ./restore.sh r2:latest. Do one test restore in the first month.