Skip to content
mango·agentby Mangosteen Studio
Server setup · Any VM

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.

$6–12 / month (free on GCP e2-micro) You run updates and backups Needs a key in GOOGLE_CREDENTIALS_JSON (none on GCP)

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#

bash
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 enable

Postgres and Redis are never exposed; only ports 22, 80 and 443 are open.

3Get the code and configure#

bash
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, Redis

For a private repository, give the server a read-only GitHub deploy key.

4Build and start#

bash
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#

bash
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 /seed

6Backups 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.

bash
./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 --build

If 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.