Self-Hosting

Build a Self-Hosted Background-Removal API with rembg and DigitalOcean Spaces

July 26, 202615 min read

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

code
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

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

bash
python -c "from rembg import new_session; new_session('u2net')"

5. The FastAPI Service

Create ~/app/main.py:

python
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:

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

json
{
  "Rules": [
    {
      "ID": "expire-results",
      "Status": "Enabled",
      "Filter": { "Prefix": "results/" },
      "Expiration": { "Days": 1 }
    }
  ]
}

Apply it with the AWS CLI (S3-compatible):

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

ini
[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
bash
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

bash
sudo apt install -y nginx certbot python3-certbot-nginx

/etc/nginx/nginx.conf (in the http block):

nginx
limit_req_zone $binary_remote_addr zone=bg:10m rate=30r/m;

/etc/nginx/sites-available/bgremover:

nginx
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;
    }
}
bash
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

bash
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_SECONDS to 300 (5 min) if links only need to survive a single download.

This mirrors ToolsHubKit's local-first philosophy: process, deliver, forget.


Related Guides


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