EN | RU
Self-hosted file sharing with dual links: one public, one LAN. Every file takes the shortest path to whoever you sent it to.
The goal of this project is a sharing service that works equally well inside a home network and on a public internet domain — at the same time, from one instance.
Most self-hosted sharing tools assume a single canonical URL. But when the server lives in your home, that assumption breaks down:
- The public domain resolves to your router's external IP. From inside the LAN, that means hairpin NAT — which some routers don't support at all, and others route slowly.
- Even when it works, sending a file to the computer in the next room via your ISP's uplink is silly. A direct LAN transfer runs at full local speed.
- And if your internet connection is down, the local link keeps working.
OmniShare treats both worlds as first-class: it knows both the public and the LAN address of the server, generates both URLs for every file, and lets you copy whichever fits the recipient.
- Dual share links — public (
https://share.example.com/f/…) and local (http://192.168.x.x:8010/f/…) for every upload. - Resumable uploads — implements the TUS protocol, so large uploads survive dropped connections and resume where they left off.
- Link lifecycle controls — optional expiry time and download limit per file; expired files are deleted automatically (on access and by a background sweep).
- Captions — attach a short description to any file.
- Multi-user — admin panel for creating users, per-user storage quotas, one-click password resets with a forced change on next login.
- Zero-config startup — the root admin account is created on first boot;
network settings can be changed from the admin panel at runtime and are
persisted back to
.env. - Simple deployment — a single container with SQLite (no external database), plus an optional bundled Caddy that obtains and renews a Let's Encrypt certificate automatically.
- Web UI — a Vue 3 single-page app, developed in frum1/omnishare_frontend.
Uploading requires an account. There is no open registration — the admin creates users from the admin panel, and each gets a storage quota.
Downloading does not. Share links are unauthenticated: anyone holding the URL can fetch the file. Treat a link as the credential. Where that isn't enough, give the file an expiry time, a download limit, or both.
One instance, two ways in. Remote recipients come through your domain and the Caddy reverse proxy; devices on your home network hit the app directly by LAN IP, no internet round-trip involved:
flowchart LR
subgraph internet [Internet]
R1[Recipient outside]
end
subgraph home [Home network]
R2[Recipient on the LAN]
subgraph server [Your server]
C["Caddy :443<br/>(auto HTTPS)"]
O["OmniShare app<br/>:LOCAL_PORT"]
end
end
R1 -- "https://share.example.com/f/abc" --> C
C -- "reverse proxy" --> O
R2 -- "http://192.168.1.10:8010/f/abc" --> O
The local URL is set with LOCAL_BASE_URL (your server's LAN address). The
public URL is whatever you configure in PUBLIC_BASE_URL — typically a domain
fronted by the bundled Caddy, but any reverse proxy works.
No need to clone the repository — two files are enough:
mkdir omnishare && cd omnishare
curl -fsSLO https://raw.githubusercontent.com/frum1/omnishare/main/docker-compose.yml
[ -f .env ] || curl -fsSL https://raw.githubusercontent.com/frum1/omnishare/main/.env.example -o .envOpen .env and set LOCAL_BASE_URL to your server's LAN address,
including the port — for example http://192.168.1.10:8010. Everything else
has sane defaults, and PUBLIC_BASE_URL can stay empty until you're ready to
put the instance on a domain (see Public HTTPS).
docker compose up -dThat's the whole setup for a LAN / HTTP (not recommended) deployment. The image is pulled from ghcr.io/frum1/omnishare.
Don't use
localhostinLOCAL_BASE_URL. Alocalhostlink only ever resolves on the server itself — open it on your phone and the phone will try to serve it. Use the machine's LAN IP, and pin it in your router's DHCP settings so it doesn't drift.
The container uses host networking, so the service listens on the host
directly — no port mapping needed, it's reachable on LOCAL_PORT (the example
.env sets 8010). Host networking is a Linux feature; on Docker Desktop for
macOS or Windows, replace network_mode: host with a ports: ["8010:8010"]
mapping.
Persistent data lives in bind mounts next to the compose file:
| Path | Contents |
|---|---|
data/ |
SQLite database + auto-generated secret_key |
storage/ |
Uploaded files |
.env |
Settings — bind-mounted (not just env_file:) because changes made in the admin panel are written back into it |
Because the admin panel rewrites .env, the file must be writable by the
container. Note that a rewrite normalises the file: comments and formatting
you added by hand may not survive it.
On first boot the server creates a root admin account and prints its
generated password to the console:
docker compose logs omnishareLog in with it and you'll be prompted to set a new password. Lost it? Reset it with the following command:
docker compose exec omnishare python -m scripts.reset_admin_passwordTo expose OmniShare on the internet under your own domain with a trusted certificate, enable the bundled Caddy reverse proxy — it obtains and renews a Let's Encrypt certificate automatically.
Prerequisites:
- A domain (or subdomain) with DNS pointed at this server's public IP.
- Ports 80 and 443 forwarded to this machine on your router/firewall (80 is required for the ACME challenge, not just for redirects).
Setup — two lines in .env, then the same up:
# in .env:
# PUBLIC_BASE_URL=https://share.example.com
# COMPOSE_PROFILES=proxy
docker compose up -dCOMPOSE_PROFILES=proxy brings up a caddy container alongside omnishare;
its config is inlined in docker-compose.yml and uses PUBLIC_BASE_URL as
the site address, so Caddy proxies to the app and manages the certificate on
its own. Remove that line for a plain-HTTP / LAN-only setup.
Since both containers share the host's network namespace, Caddy reaches the app over loopback rather than by service name:
{$PUBLIC_BASE_URL} {
reverse_proxy localhost:{$LOCAL_PORT}
}Swap in your own proxy if you prefer — anything that forwards to
localhost:LOCAL_PORT and sets X-Forwarded-Proto will do.
With host networking the app binds to every interface, including the public one. Once you forward 80/443 at the router, double-check that
LOCAL_PORTisn't reachable from outside — otherwisehttp://<public-ip>:8010serves the app over plain HTTP, bypassing Caddy and your certificate entirely.Restrict it to the LAN:
sudo ufw allow from 192.168.0.0/16 to any port 8010 proto tcp sudo ufw deny 8010/tcpAfter that the only route in from the internet is Caddy on 443.
All settings live in .env — see .env.example for the full
annotated list (public/local URLs, port, file size limit, cleanup interval,
token lifetime, and more). The essentials:
| Variable | Default | Description |
|---|---|---|
LOCAL_BASE_URL |
— | The server's LAN address, used to build local links. Include the port, e.g. http://192.168.1.10:8010. |
LOCAL_PORT |
8010 |
The port the app binds to. Changing it does not change LOCAL_BASE_URL — keep the two in sync yourself. |
PUBLIC_BASE_URL |
— | The public URL for share links, and Caddy's site address when the proxy profile is enabled. No port unless it's non-standard: https://share.example.com, not …:443. |
LOCAL_MODE |
true |
Whether the UI offers a local-network link alongside the public one. Set false on a VPS, where there is no meaningful LAN. |
Network settings can also be changed at runtime from the admin panel; changes
are written back to .env so they survive restarts.
Clone the repository and install the backend dependencies:
git clone https://github.com/frum1/omnishare.git
cd omnishare
uv sync
cp .env.example .env
# edit .env: PUBLIC_BASE_URL, etc. (see inline comments)The backend serves the web UI from dist/, which is built in a separate
repository — grab the latest build from the
frum1/omnishare_frontend releases
(the frontend-dist-*.tar.gz asset) and unpack it:
gh release download --repo frum1/omnishare_frontend --pattern '*.tar.gz'
mkdir -p dist && tar -xzf frontend-dist-*.tar.gz -C dist && rm frontend-dist-*.tar.gzThen run the server:
uv run main.pyTo build the Docker image from source instead of pulling it from GHCR,
uncomment build: . in docker-compose.yml (the build fetches the frontend
release on its own).
Swagger/ReDoc are disabled by default to reduce the attack surface on a
publicly-exposed instance. Create an empty .dev-mode file in the project
root to enable them at /docs and /redoc:
touch .dev-modeThe check happens once at startup, so restart the server after adding or removing the file.
The web UI is a separate project:
frum1/omnishare_frontend
(Vue 3 + PrimeVue). The backend serves its built output from dist/; the
Docker build fetches the latest frontend release automatically, so you only
need that repo if you're working on the UI itself.
- Backend: FastAPI + async SQLAlchemy on SQLite, JWT auth, uv for dependency management.
- Uploads: TUS 1.0 (creation & termination extensions), streamed to disk
in chunks; files are stored in a date + id-sharded tree under
storage/. - Frontend: Vue 3 + PrimeVue SPA, served by the backend.
- Deployment: Docker (host networking), optional Caddy profile for automatic HTTPS; images published to GHCR on every version tag.
GPL-3.0 — copyleft, and proudly so.
