Files
projectsend/DOCKER.md
T
ignacionelson 6e47d76ba6 ProjectSend 2.0.0
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.
2026-08-14 01:38:12 -03:00

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/ and previews/ (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:

  1. Dump the database and copy /srv/projectsend/, .env and the dump to the new machine.
  2. Install Docker, put the project directory in place, restore both as described under Restoring.
  3. Point DNS at the new machine, and update APP_URL in .env if 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.