rclone is an excellent tool for moving data to and from cloud storage, but running it on a schedule means editing command lines and cron files by hand. I wanted a web interface for it that I could deploy on my own homelab server, and I couldn't find a good option for that.
So I built this one with functionality first. The goal is that anything rclone can do for a sync, copy, or move, you can control from the job form, without dropping to a terminal. That means exclude patterns, extra flags, per-job bandwidth, file comparison method, and transfer ordering are all exposed, with sensible defaults so simple jobs stay simple.
Backups are rarely uniform. A single job can hold a few huge files and many small ones, and the order rclone transfers them in matters: either the big files finish last, or the small ones sit idle behind them. That's why the ordering options (see Why size + mixed ordering?) are a first-class part of the job form, and why the dashboard and failure list are built to stay readable with thousands of files in a run.
It runs in Docker on a home server, or as a Windows app for a desktop PC. Both use the same web UI.
Caution
This app has no authentication. Anyone who can reach its port can browse
the mounted filesystem, create jobs, and run sync/move operations against
your data (which is mounted read-write). Cross-site requests from other
websites are rejected, but that doesn't protect against direct access.
- By default the port is published on
127.0.0.1only. - Only set
BIND_ADDRESS=0.0.0.0on a network you fully trust. - Never expose it to the internet. If you need remote access, put it behind a VPN (WireGuard, Tailscale) or a reverse proxy that enforces authentication.
A Flask web GUI for managing rclone backup jobs, on a home server with Docker or on a Windows PC as a tray app. Create sync/copy/move jobs, schedule them with cron, watch live transfer progress, and browse run history — from a browser, or from the Windows app's own window.
This is a standalone app (not the upstream rclone web GUI). It never shells out to rclone; every operation goes through the rclone RC HTTP API.
![]() |
![]() |
![]() |
![]() |
(Screenshots use demo data served by a stub rclone.)
- Jobs — CRUD for sync/copy/move jobs with source/destination (local
paths or remotes), exclude patterns, extra flags, per-job bandwidth rules, and
--order-bycomposition (fieldsize/name/modtime, directionasc/desc/mixedwith a window %). The default, size + mixed, keeps small and large files moving at the same time — see Why size + mixed ordering?. - File comparison method — per job: default (modtime + size),
--size-only,--checksum, or--ignore-existing. - Directory browser — the Browse button on the job form opens a modal
that walks local paths and remotes via
operations/list. - Schedules — per-job cron schedules (APScheduler), enable/disable,
interpreted in your configured
TIMEZONE. A run that comes due while the server is down starts if it's less than an hour late (Docker); the Windows app instead starts one catch-up run for everything missed. - Live dashboard — SSE-driven progress bars, speed, bytes, file counts (new vs unchanged), and ETA.
- Failures — per-file errors from rclone are saved while a run is still going, not just the error count. The Running page shows a live feed of the newest failures; the run's history page lists them all, grouped by cause, with a filter (by file or message), paging, and a CSV export. Up to 50,000 failures are kept per run.
- History — every run is recorded with final stats and status
(
completed/failed/stopped/interrupted). - Auto-resume — per-job toggle. Orphaned
runningrecords are re-dispatched on container restart; failed runs retry after a delay up tomax_retries(default 3). Each retry is a new history record linked viaretry_of_run_id. User-stopped runs are never auto-resumed. A manual Resume button is available on failed/interrupted runs. - Settings — configured remotes list, guided setup for all of rclone's storage types (with browser sign-in for Google Drive, OneDrive, Dropbox, ...) and global bandwidth limits.
- Bandwidth limits — any number of weekly rules, each with days, a time
window (overnight windows work), and a speed in KiB/s, MiB/s, GiB/s or
Mbit/s. Set them per job (job edit page) or globally (Settings), where a rule
can apply to everything, to all remotes — including remotes added later — or
to specific remotes. A job counts as using a remote if its source or
destination is on it. Where rules overlap, the lowest limit wins. rclone has a
single speed limiter shared by all its transfers, so the app sets it as runs
start and stop and as windows open and close; jobs running at the same time
share the lowest limit that applies to any of them. Times use
TIMEZONE. - Themes — eight colour themes (dark and light) from the Theme menu in the navigation bar. The choice is stored in your browser, so each browser picks its own.
If a backup contains both huge files (videos, disk images) and thousands of tiny ones (photos, documents, source code), the order rclone transfers them in matters a lot:
| Order | What happens |
|---|---|
| Default (directory order) | Arbitrary. Big files can start late and become a long tail at the end. |
Largest first (size,desc) |
Every worker is busy on huge files, so the small files sit idle in the queue — you see few "new files" for hours, and per-file overhead (API calls, round trips) goes unused. |
Smallest first (size,asc) |
Thousands of small files finish quickly, but the huge files only start at the end, so the job's final stretch is a few slow, bandwidth-bound transfers. |
Mixed (size,mixed,50) |
Half of the transfer workers take the smallest remaining file, the other half take the largest. Small files keep flowing while the big ones are also being processed. |
The mixed window is the percentage of workers that go smallest-first (default
50); the rest go largest-first, and each group keeps its direction for the
whole run. For example, with Transfers = 4 and size,mixed,50, two workers
chew through small files while two work on large ones. Use a lower percentage
(say 25) if you want most workers on the big files, or a higher one if you
want small files to finish sooner.
Set it per job under Order By Field / Order Direction / Mixed Window (%) in
the job form; it is passed to rclone as --order-by size,mixed,50. Because the
split is across transfer workers, it works best with several parallel
transfers, and rclone's own docs note that ordering is more accurate with a
larger --max-backlog.
A regular Windows program with its own window. It runs without Docker or Python and includes rclone. Closing the window leaves it in the system tray so scheduled syncs keep running; choose Quit from the tray icon's menu to exit.
- Download
rClone-GUI-Setup.exefrom the releases page. No admin rights are needed. The installer is unsigned, so SmartScreen warns on first run: choose More info, then Run anyway. - Leave Start rClone GUI when I log in ticked if you want schedules to keep running.
- Right-click the tray icon and choose Configure remotes… to add cloud remotes. Existing rclone remotes are picked up automatically.
If the PC was asleep or off when a schedule came due, the run starts once when the app is next running, however many were missed. The PC stays awake during a transfer, but the app can't wake a sleeping PC, and schedules only run while you are logged in. Don't use it on a PC shared with people you don't trust: the web UI has no login, and other accounts on the PC can reach it.
Data is kept in %LOCALAPPDATA%\rClone GUI and is left in place on uninstall.
Password-encrypted rclone configs aren't supported yet.
See docs/standalone-windows.md for the design and build steps.
Requires Docker with the Compose plugin, and an existing rclone config
(rclone config) if you want to use cloud remotes.
cp .env.example .env
python3 -c "import secrets; print(secrets.token_hex(32))" # paste into SECRET_KEY
# Edit .env: SECRET_KEY, TIMEZONE, RCLONE_CONFIG, DATA_PATH, (PORT, BIND_ADDRESS)
docker compose up -d --buildThen open http://127.0.0.1:${PORT} (default 8080). The container
auto-restarts (unless-stopped).
The container refuses to start without a real SECRET_KEY; placeholder
values are rejected.
| Key | Default | Description |
|---|---|---|
SECRET_KEY |
— (required) | Flask secret key. Generate a random value. |
PORT |
8080 |
Host port for the web GUI |
BIND_ADDRESS |
127.0.0.1 |
Host address the port is published on. 0.0.0.0 exposes it to your whole network — see the warning above. |
TIMEZONE |
UTC |
IANA timezone (e.g. America/New_York) used to display times and to interpret cron schedules |
RCLONE_CONFIG |
~/.config/rclone |
Host path to the rclone config directory (remotes are defined here) |
DATA_PATH |
/mnt/storage |
Host data path, mounted at /mnt/ inside the container |
ALLOWED_HOSTS |
any | Comma-separated host:port values the app answers to, e.g. nas.lan:9020. Setting it blocks DNS rebinding attacks |
Example for a home server you want to reach from other machines on your LAN:
PORT=9020
BIND_ADDRESS=0.0.0.0
TIMEZONE=America/New_York
DATA_PATH=/mnt./data->/app/instance— SQLite database (gitignored)${RCLONE_CONFIG}->/root/.config/rclone— rclone remotes/config. This contains your cloud credentials; keep it out of version control.${DATA_PATH}->/mnt/(read-write) — the host data hierarchy. Note: this gives the container write access to everything underDATA_PATH;moveoperations to local destinations rely on this. Point it at the narrowest directory that works for you.
The database schema is created and upgraded by the app itself on startup
(db.create_all() plus _ensure_columns() in app/__init__.py), so there is
no separate migration step. Just docker compose up -d --build.
If you previously ran a version where TIMEZONE was not set in
docker-compose.yml, note that cron schedules used the container's clock
(UTC) while times were displayed in America/New_York. Schedules now run in
TIMEZONE, so set it explicitly and double-check your cron expressions.
Browser <--HTTP/SSE--> Gunicorn (gevent, 1 worker) :8080 <--RC HTTP--> rclone rcd :5572
|
SQLite (./data)
Both rclone rcd and Gunicorn run in a single container, started by
entrypoint.sh. The rclone RC API is bound to
127.0.0.1 inside the container and protected by a password generated on every
start, so only the Flask app can reach it. (rclone only allows adding remotes
through an authenticated API.)
Key design rules:
- Nothing runs unless the user created it. The app only tracks runs in
JobRunner._active_runs, and the SSE endpoint polls rclone per stats group (core/stats?group=job_{run_id}), never the global stats, so unrelated rclone activity never shows up. - All rclone operations are async (
_async: true) and return ajobid; the app never blocks on multi-hour transfers. - Each run gets a stats group
job_{run_id}for per-job metrics. SSE and the dashboard depend on this naming. - Gevent is mandatory (SSE connections hold greenlets).
wsgi.pycallsgevent.monkey.patch_all()first; don't use stdlibthreading/queue. - Gunicorn runs with
workers = 1— the in-memory_active_runsdict and the APScheduler monitor must live in a single process (seegunicorn.conf.py).
- Start ->
POST /ops/start/<job_id>->JobRunner.start_job() - A
RunHistoryrow is created (statusrunning) and rclone is called with_async=trueand_group=job_{run_id} - The SSE endpoint polls
core/stats?group=job_{run_id}(every 2s while active, 5s when idle) - A 10-second monitor copies new per-file failures into the DB while a run is active, then finalizes completed runs and writes their final stats
- Auto-resume (if enabled) re-dispatches orphaned/failed runs
app/
├── __init__.py # App factory, startup checks, _ensure_columns() schema upgrades
├── config.py # Env-based config (SECRET_KEY, TIMEZONE, RCLONE_RC_URL, ...)
├── extensions.py # SQLAlchemy instance
├── models.py # Job, Schedule, RunHistory (SQLite)
├── rclone_client.py # HTTP wrapper for rclone RC API
├── routes/
│ ├── jobs.py # CRUD for job definitions + directory browse API
│ ├── operations.py # Start/stop/resume/status of running syncs
│ ├── schedules.py # Cron schedule CRUD + enable/disable
│ ├── history.py # Past run list + detail view
│ ├── run_errors.py # Per-run failure list (JSON, paged), summary, CSV export
│ ├── sse.py # SSE endpoint — polls rclone directly (no bg thread)
│ └── settings.py # Remotes list, global bandwidth rules
├── services/
│ ├── job_runner.py # Core orchestration: start/stop jobs, finalize runs
│ ├── run_errors.py # Turns rclone transfer errors into stored, grouped failures
│ └── scheduler_service.py # APScheduler cron triggers
├── security.py # Cross-site POST and ALLOWED_HOSTS checks
├── templates/ # Jinja2, Bootstrap 5 (themed via data-theme)
└── static/ # CSS, vendored Bootstrap, vanilla JS (SSE, errors, themes)
desktop/ # Windows app: WebView2 window, tray icon, rclone supervisor
packaging/ # PyInstaller spec, Inno Setup script, build.ps1
tests/ # pytest suite (no rclone needed)
Tech stack: Flask 3.1, SQLAlchemy (SQLite), APScheduler 3.10, Gunicorn + gevent, Bootstrap 5 (vendored, no CDN) and vanilla JS (no build tools). The Windows app adds pywebview, pystray, Pillow and PyInstaller.
- No authentication — intended for a trusted network only. Blueprints are
structured so Flask-Login can be added later (
@login_required). - No CSRF protection (would need Flask-WTF).
- The directory browser (
/jobs/api/browse) and jobextra_flagslet any user of the app list arbitrary paths and set arbitrary rclone options. - The data path is mounted read-write, and mounted rclone config contains your remote credentials.
- The published port defaults to loopback; widening it is an explicit opt-in
(
BIND_ADDRESS). - The rclone RC API is bound to loopback inside the container only, behind a per-start password.
Found a security problem? Please open an issue (or contact the maintainer privately for anything sensitive).
python -m venv .venv && source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements-dev.txt
pytestThe tests use an in-memory SQLite database and a fake rclone client, and do
not start any background scheduler threads (BACKGROUND_TASKS_ENABLED = False).
CI runs the test suite and a Docker build on every push and pull request.
Gotchas:
exclude_patterns/extra_flagsare JSON strings in SQLite; usejob.get_excludes()/job.get_extra_flags().RunHistorydoes not snapshot job config; the detail page shows the current job config.- Move operations may not resume cleanly after an interruption (partial file relocation); a warning is shown in the job form.
- New columns on existing tables need an
ALTER TABLEentry in_ensure_columns()(create_all()won't add them). - APScheduler jobs run outside a request, so DB access needs
app.app_context().




