Build a Self-Hosted Background-Removal API with rembg and DigitalOcean Spaces
Disclosure: Some links in this guide are affiliate links. If you sign up for DigitalOcean through them, ToolsHubKit earns a small commission — at no cost to you.
Per-image background-removal APIs get expensive fast and require uploading every image to a third party. Running rembg (built on the U²-Net segmentation model) on your own Droplet, with results stored in DigitalOcean Spaces, gives you flat cost, full privacy, and short-lived signed download links. This guide builds the whole pipeline: upload → process → store in Spaces → signed URL → auto-expire.
Prerequisites:
- Basic Linux comfort (SSH,
sudo, Python) - A domain/subdomain for the API (recommended for HTTPS)
1. The Architecture
Client
│ POST /remove-bg (multipart image)
▼
FastAPI service on a Droplet
│ rembg (U²-Net) strips the background
▼
Upload PNG (transparent) to DigitalOcean Spaces
│ generate a pre-signed GET URL (expires in N minutes)
▼
Return { url } to the client
│
└─ Lifecycle rule auto-deletes the object after 24h
No image is retained on the Droplet after processing, and Spaces objects self-destruct — privacy-first by design.
2. Provision the Droplet
Background removal is CPU-friendly for moderate volume — you don't need a GPU for rembg's default model. Create a standard Droplet. Get $200 in free credits for 60 days when you sign up.
Sizing:
| Throughput | RAM | vCPUs | Droplet |
|---|---|---|---|
| Low / testing | 2 GB | 1 | Basic $12/mo |
| Steady production | 4 GB | 2 | Basic $24/mo |
| High volume | 8 GB | 4 | Basic $48/mo |
rembg loads a ~170 MB model into memory; 2 GB is the practical minimum, 4 GB is comfortable. Choose Ubuntu 24.04 LTS x64 and add your SSH key.
3. Create a Spaces Bucket
In the DigitalOcean console go to Spaces Object Storage → Create a Spaces Bucket.
- Region: same as your Droplet (lower latency, no cross-region egress)
- Name: e.g.
yourapp-bgremoval - File listing: Restrict (private bucket — access only via signed URLs)
Then create an access key: API → Spaces Keys → Generate New Key. Save the access key and secret — you'll set them as environment variables.
Note your endpoint, which looks like: https://<region>.digitaloceanspaces.com
4. Install rembg and Dependencies
ssh root@YOUR_SERVER_IP
adduser bgremover
usermod -aG sudo bgremover
su - bgremover
sudo apt update
sudo apt install -y python3-venv python3-pip
python3 -m venv ~/bg-env
source ~/bg-env/bin/activate
pip install --upgrade pip
pip install "rembg[cpu]" fastapi "uvicorn[standard]" python-multipart boto3 pillow
The first run downloads the U²-Net model (~170 MB) and caches it. Pre-warm it:
python -c "from rembg import new_session; new_session('u2net')"
5. The FastAPI Service
Create ~/app/main.py:
import os
import io
import uuid
import boto3
from botocore.config import Config
from fastapi import FastAPI, UploadFile, File, Header, HTTPException
from fastapi.responses import JSONResponse
from rembg import remove, new_session
from PIL import Image
API_KEY = os.environ["BG_API_KEY"]
SPACES_KEY = os.environ["SPACES_KEY"]
SPACES_SECRET = os.environ["SPACES_SECRET"]
SPACES_REGION = os.environ.get("SPACES_REGION", "nyc3")
SPACES_BUCKET = os.environ["SPACES_BUCKET"]
SPACES_ENDPOINT = f"https://{SPACES_REGION}.digitaloceanspaces.com"
URL_TTL = int(os.environ.get("URL_TTL_SECONDS", "900")) # 15 minutes
app = FastAPI(title="Private Background Removal API")
session = new_session("u2net") # load model once
s3 = boto3.client(
"s3",
region_name=SPACES_REGION,
endpoint_url=SPACES_ENDPOINT,
aws_access_key_id=SPACES_KEY,
aws_secret_access_key=SPACES_SECRET,
config=Config(signature_version="s3v4"),
)
def check_key(authorization: str | None):
if authorization != f"Bearer {API_KEY}":
raise HTTPException(status_code=401, detail="Invalid or missing API key")
@app.post("/remove-bg")
async def remove_bg(
file: UploadFile = File(...),
authorization: str | None = Header(default=None),
):
check_key(authorization)
raw = await file.read()
if len(raw) > 15 * 1024 * 1024: # 15 MB cap
raise HTTPException(status_code=413, detail="Image too large")
# Strip the background -> transparent PNG
input_image = Image.open(io.BytesIO(raw))
output_image = remove(input_image, session=session)
buf = io.BytesIO()
output_image.save(buf, format="PNG")
buf.seek(0)
key = f"results/{uuid.uuid4()}.png"
s3.upload_fileobj(
buf,
SPACES_BUCKET,
key,
ExtraArgs={"ContentType": "image/png", "ACL": "private"},
)
# Short-lived signed download URL
url = s3.generate_presigned_url(
"get_object",
Params={"Bucket": SPACES_BUCKET, "Key": key},
ExpiresIn=URL_TTL,
)
return JSONResponse({"url": url, "expires_in": URL_TTL})
@app.get("/health")
async def health():
return {"status": "ok"}
Run it:
export BG_API_KEY="$(openssl rand -hex 24)"
export SPACES_KEY="your_spaces_access_key"
export SPACES_SECRET="your_spaces_secret"
export SPACES_REGION="nyc3"
export SPACES_BUCKET="yourapp-bgremoval"
echo "API key: $BG_API_KEY"
uvicorn app.main:app --host 0.0.0.0 --port 8000
6. Auto-Expire Stored Images
Set a lifecycle rule on the bucket so results self-delete — no manual cleanup, no indefinite retention. Create lifecycle.json:
{
"Rules": [
{
"ID": "expire-results",
"Status": "Enabled",
"Filter": { "Prefix": "results/" },
"Expiration": { "Days": 1 }
}
]
}
Apply it with the AWS CLI (S3-compatible):
pip install awscli
aws s3api put-bucket-lifecycle-configuration \
--bucket yourapp-bgremoval \
--lifecycle-configuration file://lifecycle.json \
--endpoint-url https://nyc3.digitaloceanspaces.com
Every processed image now vanishes after 24 hours automatically.
7. Run as a systemd Service
Create /etc/systemd/system/bgremover.service:
[Unit]
Description=Background Removal API
After=network.target
[Service]
Type=simple
User=bgremover
WorkingDirectory=/home/bgremover
Environment=BG_API_KEY=your_generated_key
Environment=SPACES_KEY=your_spaces_access_key
Environment=SPACES_SECRET=your_spaces_secret
Environment=SPACES_REGION=nyc3
Environment=SPACES_BUCKET=yourapp-bgremoval
ExecStart=/home/bgremover/bg-env/bin/uvicorn app.main:app --host 0.0.0.0 --port 8000
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
sudo systemctl enable --now bgremover
sudo journalctl -u bgremover -f
For secrets, prefer an EnvironmentFile= pointing to a chmod 600 file over inline Environment= lines.
8. Nginx, HTTPS, and Rate Limiting
sudo apt install -y nginx certbot python3-certbot-nginx
/etc/nginx/nginx.conf (in the http block):
limit_req_zone $binary_remote_addr zone=bg:10m rate=30r/m;
/etc/nginx/sites-available/bgremover:
server {
listen 80;
server_name bg.yourdomain.com;
client_max_body_size 20M;
location / {
limit_req zone=bg burst=10 nodelay;
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_read_timeout 120s;
}
}
sudo ln -s /etc/nginx/sites-available/bgremover /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
sudo certbot --nginx -d bg.yourdomain.com
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable
9. Using the API
API=https://bg.yourdomain.com
KEY=your_generated_key
curl -s -X POST "$API/remove-bg" \
-H "Authorization: Bearer $KEY" \
-F "file=@product-photo.jpg" | jq
# { "url": "https://...signed...", "expires_in": 900 }
The returned URL points straight at Spaces, so the download doesn't hit your Droplet's bandwidth — Spaces serves it directly.
10. Cost Model: Spaces + Droplet vs Per-Image SaaS
| Component | What you pay |
|---|---|
| Droplet | Flat monthly (e.g. $24/mo for the 4 GB tier) |
| Spaces storage | Base plan includes a storage + transfer allowance |
| Spaces egress | Cheap per-GB past the included allowance |
| Per-image SaaS | A fixed price per image, forever |
With SaaS, 50,000 images/month is 50,000 × the per-image rate. Self-hosted, it's a flat Droplet + Spaces bill regardless of count (until you saturate CPU, at which point you scale the Droplet). Break-even usually lands in the low thousands of images per month.
Because auto-expiry deletes results after 24h, your Spaces storage stays near-constant no matter how many images you process — you only ever store one day's worth.
Re-verify current Spaces and Droplet pricing before publishing your own cost figures.
11. Privacy-First Patterns
- No-retention by default — the Droplet deletes the upload from memory after processing (nothing written to disk), and Spaces objects expire in 24h.
- Signed URLs only — the bucket is private; images are reachable solely through short-lived signed links.
- Shorten the TTL — drop
URL_TTL_SECONDSto 300 (5 min) if links only need to survive a single download.
This mirrors ToolsHubKit's local-first philosophy: process, deliver, forget.
Related Guides
- Run your own Whisper transcription API on a GPU Droplet — the same self-hosted-inference pattern for audio
- Self-host your entire developer stack on one Droplet — add this service to a multi-app box
- GitHub Actions auto-deploy to a Droplet — automate deployments of this API
Resources
| Resource | Link |
|---|---|
| DigitalOcean Droplets & Spaces (+ $200 free credit) | Sign up here |
| rembg | https://github.com/danielgatis/rembg |
| DigitalOcean Spaces docs | https://docs.digitalocean.com/products/spaces/ |
| boto3 (S3 SDK) | https://boto3.amazonaws.com/v1/documentation/api/latest/index.html |
| FastAPI | https://fastapi.tiangolo.com |