Client file sharing, rebuilt from the ground up: a private area per client, resumable uploads, folders, groups and categories, sharing with expiry dates and download limits, comments, file versions, an activity log, a REST API, and sixteen languages. This repository begins here. ProjectSend 2 was developed privately, and that development history is not published — the previous generation remains available, with its own history, at projectsend/legacy. Free software under the GNU General Public License v2, or (at your option) any later version.
9.3 KiB
Running ProjectSend with Docker: where your data lives
Docker is the recommended way to run ProjectSend, and this page is about the one part of it that
bites people: your files and your database do not belong to the containers, and you should be
able to prove it. Containers are meant to be thrown away and rebuilt — that is the whole point of
them — so an upgrade, a crash, or a bad docker compose command must never be able to take your
data with it.
Read this before you put real files in ProjectSend, not after.
Getting started with Docker in the first place is covered in README. Installing without Docker, on a plain PHP server, is INSTALL.md.
The three things that matter
Everything ProjectSend cannot regenerate lives in exactly three places:
| What | Where it is by default | Losing it means |
|---|---|---|
| The database | A Docker named volume, projectsend_db-data |
Everything except the files themselves: accounts, groups, permissions, share links, comments, the activity log |
| Uploaded files | storage/app/files/ in the project directory |
The files your clients downloaded — gone |
.env |
The project directory | APP_KEY, without which saved SMTP and LDAP passwords cannot be decrypted |
You do not have to work out which of these you have from memory. The dashboard's System panel reports where your uploaded files actually live — a host directory, a Docker volume (named), or the container's own filesystem — and warns you about the last two. The database is the one thing it cannot check: it runs in its own container, and the only way for PHP to see inside that one would be to hand it the Docker socket, which would turn any vulnerability in the application into root on your server. That half is on you, and it is what the backup section below is for.
Two things you may be surprised to find you do not need to protect:
- Redis (
projectsend_redis-data) holds sessions, the cache and the job queue. Losing it signs everyone out and drops any not-yet-sent emails or half-built zips. Annoying; not data loss. - Parts of
storage/app/files/are derived, not precious:zips/(built downloads, deleted automatically after a day),thumbnails/andpreviews/(rebuilt on demand the next time somebody looks at a file). They sit in the same directory as the real uploads, so the simplest thing is to back up all of it and not think about which is which.
The good news, and the one command to fear
Named volumes are already outside the container lifecycle. docker compose down,
docker compose up --build, deleting and recreating every container — none of those touch
projectsend_db-data. Upgrading does not lose your database, and never did.
The command that does destroy it is:
docker compose down -v # ← the -v deletes the named volumes
That flag exists to clean up a development machine. On a real installation it deletes your entire
database in about a second, with no confirmation. The same goes for docker volume prune and
docker system prune --volumes when the stack happens to be down.
So the actual problem with the default setup is not fragility, it is invisibility: your
database is somewhere under /var/lib/docker/volumes/, which means most people never back it up
and would not know where to look. The rest of this page fixes that.
Putting the data where you chose
Bind-mount both to real paths on the host, so your data sits somewhere you picked, somewhere you
can see in ls, and somewhere your existing backup tool already knows about.
1. Make the directories
sudo mkdir -p /srv/projectsend/files /srv/projectsend/mysql
# The app containers run as uid 1000 by default (the WWWUSER build argument).
# If you set WWWUSER to something else in .env, use that instead.
sudo chown -R 1000:1000 /srv/projectsend/files
Leave /srv/projectsend/mysql owned by root — the MySQL image sets its own ownership the first
time it starts.
2. Create compose.override.yaml
Next to compose.yaml. Docker Compose reads this file automatically and merges it on top, so you
never edit the tracked compose.yaml and nothing you write here is lost on the next update.
services:
# All four app containers must see the same files directory. Missing one of
# them is the classic mistake: uploads land in one place and downloads are
# served from another, so every download 404s. `web` is the one people
# forget — nginx serves the bytes itself, from
# /var/www/html/storage/app/files/, so it needs the mount just as much as
# the container that wrote them.
app:
volumes:
- /srv/projectsend/files:/var/www/html/storage/app/files
web:
volumes:
- /srv/projectsend/files:/var/www/html/storage/app/files
worker:
volumes:
- /srv/projectsend/files:/var/www/html/storage/app/files
scheduler:
volumes:
- /srv/projectsend/files:/var/www/html/storage/app/files
db:
volumes:
- /srv/projectsend/mysql:/var/lib/mysql
Check the result before applying it — this prints the fully merged configuration:
docker compose config
3. Move the data you already have
Skip this on a brand-new installation. There is nothing to move; go straight to step 4.
Stop everything first. Copying a database out from under a running MySQL is how you get a backup that restores into a corrupt table.
docker compose down # no -v
Files, which are already on the host inside the project directory:
sudo rsync -a storage/app/files/ /srv/projectsend/files/
sudo chown -R 1000:1000 /srv/projectsend/files
The database, which is in the named volume. A throwaway container is the tidy way to reach inside one:
docker run --rm \
-v projectsend_db-data:/from \
-v /srv/projectsend/mysql:/to \
alpine sh -c 'cd /from && cp -a . /to'
(projectsend_db-data is the volume's real name — the db-data from compose.yaml prefixed with
the project name. docker volume ls will confirm it.)
4. Start, and check
docker compose up -d
Then prove it worked rather than assuming: log in and check the dashboard's System panel — Files
stored on should now read Host directory, and the Docker-volume warning should be gone. Then
open a file, download it, and upload a new one; confirm the new upload appears in
/srv/projectsend/files/ on the host. A download that returns nothing means one of the four
containers is missing the mount from step 2.
Once you are satisfied, and not before, you can reclaim the old volume:
docker volume rm projectsend_db-data
Backing up
Bind mounts make your data visible. They do not make it backed up.
The database
Do not back up the MySQL directory by copying it while the database is running. A file-level copy of a live data directory is not a snapshot — it is a set of files captured at slightly different moments, and it may restore into something subtly broken. Use a dump:
docker compose exec -T db \
mysqldump -u root -p"${DB_ROOT_PASSWORD:-root}" \
--single-transaction --routines --triggers \
projectsend > projectsend-$(date +%F).sql
--single-transaction is what makes this safe on a running database: the dump sees one consistent
moment in time without locking anybody out.
The files
rsync -a /srv/projectsend/files/ /your/backup/location/files/
Ordinary files, no special handling. Restoring means copying them back and fixing ownership
(chown -R 1000:1000).
.env
Copy it somewhere safe, once, and again whenever you change it. It is a few hundred bytes and it
holds APP_KEY — lose that and the SMTP and LDAP passwords stored in your database become
undecryptable, even though the rest of the backup is perfect.
Restoring
docker compose up -d db
docker compose exec -T db mysql -u root -p"${DB_ROOT_PASSWORD:-root}" projectsend < projectsend-2026-08-08.sql
sudo rsync -a /your/backup/location/files/ /srv/projectsend/files/
sudo chown -R 1000:1000 /srv/projectsend/files
docker compose up -d
Test this at least once, on a machine that is not your live one. A backup nobody has ever restored is a hypothesis, not a backup.
Upgrading
With the data outside the containers, an upgrade touches only the containers:
docker compose down # again: no -v
git pull # or unpack the new release over the directory
docker compose up -d --build
The app container migrates the database itself on boot and verifies its reference data, so there is no separate migration step. Take a database dump first anyway — migrations move forwards, not backwards, and the one time you skip it will be the time you want it.
Moving to another server
This is the payoff for everything above, and it is worth doing once deliberately so you know it works:
- Dump the database and copy
/srv/projectsend/,.envand the dump to the new machine. - Install Docker, put the project directory in place, restore both as described under Restoring.
- Point DNS at the new machine, and update
APP_URLin.envif the address changed.
No export tool, no vendor involvement, nothing that only works while the old machine is alive. That is the property worth protecting, and the reason this page exists.