# PicoShare: A Self-Hosted File Sharing Service With Direct Download Links

> PicoShare is a Go web app that stores uploads in SQLite and hands out direct download links with no resizing, re-encoding or file type limits. Here is how to run it in Docker, what Litestream replication actually buys you, and where it stops being the right tool.

**mtlynch/picoshare** — A minimalist, easy-to-host service for sharing images and other files

- Repository: https://github.com/mtlynch/picoshare
- Stars: 3,047 · Forks: 227
- Language: Go
- License: NOASSERTION
- Published: 2026-09-24 · Updated: 2026-09-24 · Language: en
- Canonical page: https://hysenlabs.com/projects/mtlynch-picoshare

## What PicoShare Removes From File Sharing

Most file sharing services make you choose between two bad options. Image hosts re-encode your upload and give you back a link to their version. General cloud storage gives you a preview page with a download button hidden behind a signup, an interstitial, or a quota. PicoShare's README frames its whole pitch around the opposite: a direct download link you can hand to anyone, with no ads and no signups on the receiving end, and no restriction on size, file type, or media format.

The claim worth taking literally is the no-resizing one. The README states that PicoShare never resizes or re-encodes media, so a direct download link exists as soon as the upload finishes. That is a design decision with consequences: the file you get back is byte-for-byte what you sent, and the server does no transcoding work. It also means PicoShare offers no thumbnails, no format conversion, and no bandwidth savings from serving a smaller variant.

The audience is narrow and specific. It fits a developer or small team that already runs a VPS or a Fly.io app and wants a personal upload endpoint: screenshots for a bug report, a build artifact, a video file that a chat client would otherwise mangle. It is not aimed at a company replacing a document management system.

## Go Handlers, SQLite Storage, and the Litestream Sidecar

The repository layout tells most of the story. The Go module is github.com/mtlynch/picoshare, and the top-level directories are split by responsibility: handlers/ for HTTP, store/ for persistence, space/ for storage accounting, garbagecollect/ for cleanup, and cmd/picoshare/main.go as the entry point. Routing uses github.com/gorilla/mux, and persistence uses github.com/mattn/go-sqlite3, so the database is a single SQLite file rather than a separate database server. Schema changes are handled by codeberg.org/mtlynch/go-evolutionary-migrate, which implies forward migrations applied at startup.

The database location is a flag, not an environment variable: -db, defaulting to "data/store.db". The Dockerfile sets CMD ["-db", "/data/store.db"], which is why every Docker example in the README mounts a host directory at /data. Uploaded file content and metadata live behind that one path, which is the reason replication is even tractable.

Replication is delegated to Litestream rather than implemented in Go. The Docker image is built from alpine:3.15, copies a Litestream binary downloaded at build time (build arg litestream_version, default v0.3.13), copies litestream.yml to /etc/litestream.yml, and runs /app/docker-entrypoint as its entrypoint. The docker-entrypoint script is what decides whether to start Litestream alongside the app based on the LITESTREAM_* variables. That is the actual mechanism: PicoShare writes to SQLite, Litestream ships the SQLite WAL and snapshots to an S3-compatible bucket, and on restart the data is restored from that bucket. The README describes the outcome plainly: you can kill the container and start it later and PicoShare restores your data and continues as if there was no interruption.

The constraint that follows from this architecture is stated in the README as a note: only run one Docker container for each Litestream location, because PicoShare cannot sync writes across multiple instances. There is no leader election and no application-level conflict resolution. Scaling this out means replacing the storage layer, not adding replicas.

## Installing PicoShare With Docker and Uploading Your First File

The README gives three routes: go run from source, a plain docker run, and Docker Compose. The Docker route is the shortest path to something usable, and it needs two environment variables and one volume mount. PS_SHARED_SECRET is the admin passphrase; without it (or PS_SHARED_SECRET_FILE) the app has no way to authenticate the admin user.

```bash
docker run \
  --env "PORT=4001" \
  --env "PS_SHARED_SECRET=somesecretpass" \
  --publish 4001:4001/tcp \
  --volume "${PWD}/data:/data" \
  --name picoshare \
  mtlynch/picoshare
```

After the container starts, the README's demo shows the flow: open the app in a browser, log in as admin with the passphrase you set, and upload a file. What you get back is a direct download link. Anyone with that link can view or download the file without an account. The port mapping matters because PORT defaults to 4001, so publishing 4001:4001 keeps the container port and host port aligned.

If you prefer a file on disk instead of a long command, the README supplies a Compose file. Note that it passes the database path through command rather than an environment variable, and that the comment tells you to change the password.

```yaml
version: "3.2"
services:
  picoshare:
    image: mtlynch/picoshare
    environment:
      - PORT=4001
      - PS_SHARED_SECRET=dummypass # Change to any password
    ports:
      - 4001:4001
    command: -db /data/store.db
    volumes:
      - ./data:/data
```

Run docker-compose up in the directory holding that file. The ./data directory on the host becomes the home of store.db, so back that directory up or wire up Litestream before you rely on it.

Running from source is the third option, and it is the one to use if you want to patch the code. The README's example sets the same two variables inline:

```bash
PS_SHARED_SECRET=somesecretpass PORT=4001 \
  go run cmd/picoshare/main.go
```

Behind a reverse proxy, set PS_BEHIND_PROXY to "true". The README says this gives better logging when PicoShare runs behind a proxy, which is the kind of detail that matters when your proxy terminates TLS and the app needs to know the original request context.

## Where PicoShare Is the Wrong Tool

The scope statement in the README is unusually direct. PicoShare is maintained by Michael Lynch as a hobby project, and the section is titled PicoShare's scope and future, with the text cutting off at "Due to time limitations". That is the honest framing, and it should shape how you deploy it. There is no support contract, no SLA, and no promise that a feature request lands.

The multi-instance limitation is the hardest technical boundary. Because SQLite is the single source of truth and Litestream replicates rather than coordinates, you cannot run two containers against the same bucket and expect consistent writes. If your requirement is a horizontally scaled upload service behind a load balancer, PicoShare is the wrong starting point and you would be rebuilding its storage layer to get there.

The absence of server-side processing is a second boundary. If you need thumbnails, automatic image compression, video transcoding, or per-file access expiry enforced by the server, the README documents none of that. The no-re-encoding promise and the no-transformation feature set are the same decision viewed from two sides.

Finally, the README does not document a rollback or downgrade procedure. It describes forward schema migrations through go-evolutionary-migrate, but nothing about reverting a version. If you run a release and a migration applies, treat the database as forward-only until you have verified otherwise in a copy.

## PicoShare Versus a General Object Store Front End

The obvious alternative is putting a web front end on top of S3-compatible object storage yourself, or using a tool built for that. The difference in approach is where the metadata lives. PicoShare keeps file metadata and content references in a local SQLite file and treats cloud storage as a replication target through Litestream, with LITESTREAM_BUCKET, LITESTREAM_ENDPOINT, LITESTREAM_ACCESS_KEY_ID, LITESTREAM_SECRET_ACCESS_KEY, LITESTREAM_PATH (default db) and LITESTREAM_RETENTION (default 72h) as the knobs. An object-store-first design inverts that: the bucket is the source of truth and the app is stateless.

The trade-off is legible. PicoShare's model gives you a single file to back up and a container that works with no cloud account at all, since Litestream is optional. The object-store model gives you stateless instances, which is exactly what PicoShare cannot do. If you have already decided you need multiple app instances, the second model is the one that matches, and PicoShare's own note about running one container per Litestream location is the reason.

The .env.example file in the repository shows the intended Litestream setup against Backblaze B2, with BACKBLAZE_REGION, a derived LITESTREAM_ENDPOINT of s3.${BACKBLAZE_REGION}.backblazeb2.com, and LITESTREAM_PATH set to db. That is a concrete signal of the deployment shape the author tests against: one container, one bucket, one SQLite file.

## Licence, Upgrades, and What Maintenance Costs You

The repository's package.json declares "license": "AGPL-3.0" for the dev scripts, and the README's badge links to a LICENSE file with an AGPL label. The metadata supplied for the repository reports the licence as NOASSERTION, which is a detection result rather than a statement about the project, so read the LICENSE file in the repository before you rely on a specific identifier. This is not legal advice. The practical point for an adopter is that AGPL-3.0 is a copyleft licence with network-use terms, so if you modify PicoShare and expose it to users over a network, the licence's source-availability conditions are the thing to have your own counsel review.

Upgrade cost is low by construction. The image is published as mtlynch/picoshare, so an upgrade is a pull and a restart of the container with the same volume mounted. Schema migrations run through go-evolutionary-migrate, which the README does not describe in detail, so the safe pattern is to copy the data directory before pulling a new tag. The release history shows patch releases close together (1.5.2, v1.5.3, v1.5.4 within a week in May 2026), which suggests small fixes rather than long upgrade paths, but the README does not promise backward compatibility across versions.

The last push to the repository was on 2026-09-16, and the repository is not archived. The README's own wording, a hobby project maintained by one person, is the more useful signal than any activity indicator: plan for the possibility that an issue you file sits for a while.

## Conclusion

Adopt PicoShare if you want a small, single-container file sharing endpoint that returns direct download links and never re-encodes uploads, and if you accept that the README describes it as a hobby project maintained by one person. Do not adopt it if you need multi-instance horizontal scaling, server-side image transforms, or a written rollback procedure; the README states PicoShare cannot sync writes across multiple instances, and it does not document a downgrade path. Before you commit, verify two things yourself: that your reverse proxy passes the same host and scheme you expect (set PS_BEHIND_PROXY=true for better logging), and that your Litestream credentials can write to the bucket, because the docker-entrypoint and litestream.yml in the repository are where replication is wired up and the README only shows the variables.

## FAQ

### How do I run PicoShare with Docker Compose?

The README provides a docker-compose.yml that uses the mtlynch/picoshare image, sets PORT=4001 and PS_SHARED_SECRET, maps port 4001, passes -db /data/store.db as the command, and mounts ./data at /data. Run docker-compose up in the directory containing that file.

### What port does PicoShare listen on?

The PORT environment variable controls the TCP port for HTTP connections and defaults to 4001. The README's Docker and Compose examples publish 4001:4001, and the source example sets PORT=4001.

### Does PicoShare have an API?

The README documents command-line flags and environment variables but does not describe a public HTTP API for uploads or management. The only documented flag is -db, which sets the SQLite database path.

### Can I run more than one PicoShare container?

No, not against the same storage location. The README notes that you should only run one Docker container for each Litestream location because PicoShare cannot sync writes across multiple instances.

### How do I back up PicoShare data?

Uploads and metadata live in the SQLite file at the -db path, which the Docker image sets to /data/store.db, so backing up the mounted data directory captures everything. Alternatively, supplying Litestream-compatible storage settings makes PicoShare replicate automatically and restore data from that location on restart.

### Which licence does PicoShare use?

The README links to a LICENSE file with an AGPL label, and package.json declares AGPL-3.0 for the dev scripts. The repository metadata reports the licence as NOASSERTION, so check the LICENSE file itself rather than relying on the metadata.

## Sources

- [Issues](https://github.com/mtlynch/picoshare/issues)
- [mtlynch/picoshare on GitHub](https://github.com/mtlynch/picoshare)
- [README](https://github.com/mtlynch/picoshare/blob/master/README.md)
- [Releases](https://github.com/mtlynch/picoshare/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/mtlynch-picoshare
