Quick start
From a fresh Linux machine to your first upload with Docker Compose, in about ten minutes.
This guide installs Shotstash with Docker Compose on one machine and ends with a file uploaded and viewed. The target is ten minutes on a machine that already has Docker: compose pulls the published images, nothing is built on your machine.
Timing
The ten-minute target is measured from git clone to the first upload on a Linux x64 machine with Docker installed, pulling the published images. An owner-timed run on a clean server is still pending; until then treat the number as a target, not a promise.
What you need
- A machine with Docker. Linux x64 is the primary platform; linux/arm64 images are built too, on a best-effort basis. WSL2 on Windows and macOS with Docker Desktop work as well.
- Docker Engine with the Docker Compose plugin 2.24.4 or newer (
docker compose version; the preflight checks it), and git. - About 2 GB of free memory (4 GB if you add the optional search service).
- Free disk space for your media. Files live in
./data/medianext to the compose file unless you choose an S3-compatible bucket.
Install
Get the code and create .env
git clone https://github.com/Aldyernesto/shotstash.git shotstash
cd shotstash
sh docker/init.shdocker/init.sh copies .env.example to .env and gives SESSION_SECRET, MEDIA_SIGNING_SECRET, SETUP_TOKEN, WORKER_BOOTSTRAP_TOKEN and POSTGRES_PASSWORD each its own random value. It prints none of them and never overwrites an existing .env. Then it runs the preflight: Docker present, secrets valid, port free.
To do it by hand instead, run cp .env.example .env, give each of those five its own openssl rand -hex 32, and run sh docker/preflight.sh.
POSTGRES_PASSWORD is fixed when the database is first created. Changing it in .env later does not change the database, and the app can no longer connect: keep the first value, or start over with docker compose down -v.
Set the address people open
Set APP_URL in .env to the address people will type, because emails and share links use it:
http://192.168.1.20:3005for a server on your LAN,https://media.example.combehind a domain (see Networking).
Left empty, it is http://localhost:3005, which only works on the server itself.
Start
docker compose up -dAfter changing .env, sh docker/preflight.sh checks it again.
The first start pulls the published app and worker images (ghcr.io/aldyernesto/shotstash and shotstash-worker, tag latest); the COMPOSE_FILE line that docker/init.sh copied into .env tells compose to use them. docker compose ps lists the services; the app is ready when it reports healthy.
Create the owner account
Open http://localhost:3005 (or http://<server-ip>:3005). You land on /setup: it checks that storage is writable and creates the first super admin. The form asks for the SETUP_TOKEN that docker/init.sh wrote to .env (grep SETUP_TOKEN .env), so nobody else can claim a fresh instance. Without a SETUP_TOKEN, whoever submits the form first becomes the owner.
Upload a file
Create a project, open it, and drop a photo or a video on the page (or use Upload). The upload runs in parts, survives a reload and shows a thumbnail when it is done. Click the file to open the viewer. The user guide walks through the rest.
What runs
| Service | What it is | Reachable from |
|---|---|---|
app | Shotstash with ffmpeg | the host, on SHOTSTASH_PORT (default 3005) |
db | PostgreSQL 17 (volume db-data) | the compose network only |
cache | Dragonfly (rate limits, realtime events) | the compose network only |
worker | the reference processing worker (720p video proxies) | makes outgoing calls only |
Database migrations run every time the app starts. Every setting is an environment variable read at start: change .env and run docker compose up -d again, no rebuild needed. The full list is in Configuration.
Platform notes
Docker Engine and the Compose plugin from your distribution or from Docker's repository both work. The app runs as uid 1000 and fixes the owner of ./data/media at start. On NFS with root_squash or an SMB share it cannot, and the app stops with a message: make the folder writable by uid 1000 on the host (sudo chown -R 1000:1000 ./data/media, or mount the share with uid=1000,gid=1000).
Common problems
- Port already taken.
docker/preflight.shtells you. SetSHOTSTASH_PORT=8080(any free port) in.env, setAPP_URL=http://localhost:8080to match, and start again. - The app does not start.
docker compose logs appnames every bad variable; fix it in.envand rundocker compose up -d. - Share links point at localhost. Set
APP_URLto the address people open. - The pull fails with "denied" or "unauthorized" from ghcr.io. The images are public and need no login; an old or expired login gets in the way. Run
docker logout ghcr.ioanddocker compose pullagain. - The pull is slow, times out or hits a rate limit. Check that the machine reaches
ghcr.io(proxy, firewall, DNS), wait a few minutes and rundocker compose pullagain. Without network access to the registry, build from source. - "no matching manifest" during the pull. There is no published image for this machine's architecture (images exist for linux/amd64 and linux/arm64; the preflight warns about others). Build from source instead.
- The preflight says Docker Compose is too old. The published images need Docker Compose 2.24.4 or newer. Update Docker Desktop, or the
docker-compose-pluginpackage from Docker's repository on Linux, or build from source.
Build from source
The published images are the default. To build the app and worker images on your machine instead (to run a change of your own, or on a platform without images), set this line in .env:
COMPOSE_FILE=docker-compose.ymlThen start with docker compose up -d --build, and add --build to every later up as well. The first build takes several minutes and about 4 GB of memory. To go back to the published images, restore COMPOSE_FILE=docker-compose.yml:docker-compose.images.yml.
COMPOSE_FILE lists relative paths separated by : (no Windows drive letters; COMPOSE_PATH_SEPARATOR=: in .env makes that work on Windows too). While it is set, Compose no longer loads a docker-compose.override.yml by itself: append your own override to the list instead, for example COMPOSE_FILE=docker-compose.yml:docker-compose.images.yml:docker-compose.override.yml.
Optional: search
docker compose --profile search up -d adds Elasticsearch (about 1 GB of memory). Then set ELASTICSEARCH_NODE_URL=http://elasticsearch:9200 in .env and run docker compose up -d. Without it, search runs in PostgreSQL with the same results. After enabling it on an existing install, run npm run search:reindex once.
Next steps
- Put it behind HTTPS: Networking.
- Plan your backups before you upload real work: Backup and restore.
- Use a bucket instead of a disk: Storage.