Skip to content

Seafile

Seafile syncs files between devices and shares them with other people, with desktop and mobile clients and optional collaborative editing. Eight containers, split across five compose files so the optional parts can be switched off.

Last run on a host 8 Nov 2026 · data restored from a backup

Pinned at seafileltd/seafile-mc:13.0.29 (released 4 Nov 2026) · upstream has released 14.0.9 on 13 Oct 2026

In the upstream images: 3 CRITICAL · 103 HIGH with a published fix — per image

To try it: Docker, nothing else. See Try it locally.

To deploy it behind the proxy: Traefik running, a domain, and SMTP if the server should send invitations or reset links.

On your own machine, with no proxy, domain or certificate. From apps/seafile/:

Terminal window
cp .env.local.example .env.local
# The secrets ship empty; generate the ones this stack needs
{
echo "SEAFILE_DB_PASSWORD=$(openssl rand -base64 32 | tr -d '\n')"
echo "DB_ROOT_PASSWORD=$(openssl rand -base64 32 | tr -d '\n')"
echo "REDIS_PASSWORD=$(openssl rand -hex 32)"
echo "SEAFILE_ADMIN_PASSWORD=$(openssl rand -base64 32 | tr -d '\n')"
echo "JWT_PRIVATE_KEY=$(openssl rand -base64 48 | tr -d '\n')"
} >> .env.local
mkdir -p volumes/local/mysql volumes/local/redis volumes/local/seafile-data
sudo chown 999:999 volumes/local/mysql
docker compose -f docker-compose.local.yml --env-file .env.local up -d

Then open http://127.0.0.1:8000. Stop it again with the same command and down.

COMPOSE_FILE in .env decides which parts run. Removing a filename and running docker compose up -d again switches that component off.

File Services Required
seafile-server.yml the server, MariaDB, memcached, Redis yes
seadoc.yml collaborative document editor optional
notification-server.yml push notifications for file changes optional
md-server.yml file metadata and extended properties optional
thumbnail-server.yml image and video thumbnails optional
onlyoffice-callback.yml no service: the two routes OnlyOffice calls back on with OnlyOffice

All of them are served under one hostname; Traefik routes by path.

From apps/seafile/:

  1. Copy the environment file and set APP_TRAEFIK_HOST, SEAFILE_ADMIN_EMAIL and TIMEZONE:

    Terminal window
    cp .env.example .env
  2. Generate the secrets:

    Terminal window
    mkdir -p .secrets
    openssl rand -base64 32 | tr -d '\n' > .secrets/db_root_pwd.txt
    openssl rand -base64 32 | tr -d '\n' > .secrets/seafile_db_pwd.txt
    openssl rand -base64 32 | tr -d '\n' > .secrets/seafile_admin_pwd.txt
    openssl rand -base64 48 | tr -d '\n' > .secrets/jwt_key.txt
    openssl rand -hex 32 | tr -d '\n' > .secrets/redis_pwd.txt

    The Redis password is hex, not base64: Seafile builds its Redis URL from it without escaping, and a / would end the URL early.

    The thumbnail server does not read Docker secrets; it takes two of these values from .env. Left empty, every thumbnail fails while the container reports healthy:

    Terminal window
    sed -i "s|^JWT_PRIVATE_KEY=.*|JWT_PRIVATE_KEY=$(cat .secrets/jwt_key.txt)|" .env
    sed -i "s|^SEAFILE_MYSQL_DB_PASSWORD=.*|SEAFILE_MYSQL_DB_PASSWORD=$(cat .secrets/seafile_db_pwd.txt)|" .env
  3. Create the SMTP secret file even if mail stays off — compose mounts it unconditionally:

    Terminal window
    touch .secrets/smtp_pwd.txt # mail disabled
    # printf '%s' 'SMTP-PASSWORD' > .secrets/smtp_pwd.txt # mail enabled

    The OnlyOffice secret file is mounted unconditionally as well. An empty file keeps the integration off; with OnlyOffice, reuse its secret:

    Terminal window
    touch .secrets/onlyoffice_jwt_secret.txt # no OnlyOffice
    # cp ../onlyoffice/.secrets/jwt_secret.txt .secrets/onlyoffice_jwt_secret.txt # with OnlyOffice
  4. Start it. The first boot initialises the databases and runs migrations, which takes two to four minutes:

    Terminal window
    docker compose up -d
    docker compose logs seafile --follow

    Wait for Seafile server started.

  5. Recreate the server container once. On the first boot Seafile writes seahub_settings.py after the entrypoint has already run, so the blueprint block — mail, document editing, metadata, thumbnails — is not in it yet:

    Terminal window
    docker compose up -d --force-recreate seafile
  6. Confirm the block arrived:

    Terminal window
    docker compose exec seafile bash -lc \
    'grep -n "# --- Blueprint custom settings ---" /shared/seafile/conf/seahub_settings.py \
    || echo "STILL MISSING — check entrypoint logs"'
  7. Open https://<APP_TRAEFIK_HOST> and log in as SEAFILE_ADMIN_EMAIL with the password from .secrets/seafile_admin_pwd.txt.

Terminal window
docker compose ps # eight services with the default COMPOSE_FILE

Then upload a file through the web interface and sync it with a desktop client. Sync is what this stack exists for and it exercises paths the web interface does not.

If mail is enabled, invite a user and confirm the message arrives. Without the settings block from step 5, Seafile reports that the mail service is not configured.

The stack ships access-trusted-vpn, with limit-sync and headers-standard. Seafile holds whatever people put in it, and the sync clients reach it over the same hostname, so widening the access policy is a decision about every client at once.

seadoc.yml adds the collaborative editor for .sdoc files; it calls Seafile over the internal network, so the closed route does not stand in its way.

OnlyOffice is a separate stack with its own hostname — see OnlyOffice. It calls Seafile back at its public name on two paths: the document fetch under /seafhttp/files/ and the save callback /onlyoffice/editor-callback/. The stack ships closed (access-trusted-vpn), which refuses a request from a container, so onlyoffice-callback.yml opens exactly those two paths with access-public — add it to COMPOSE_FILE when you run OnlyOffice; the rest of the instance stays closed:

COMPOSE_FILE=seafile-server.yml,seadoc.yml,thumbnail-server.yml,onlyoffice-callback.yml

A Document Server on another host reaches a closed instance by its address in TRUSTED_RANGES or over the VPN instead, with the file left out.

Databases MariaDB, container seafile-db, three databases: ccnet_db, seafile_db, seahub_db, user seafile
Password .secrets/seafile_db_pwd.txt
State volumes/mysql · volumes/seafile-data (the file store) · volumes/seadoc-data
Rebuildable volumes/redis · the log directory under volumes/seafile-data
Quiescing Not needed. Seafile writes content-addressed blocks, so a file captured mid-write is a new block rather than a damaged one.

For borgmatic:

mariadb_databases:
- name: ccnet_db
container: seafile-db
username: seafile
password: "{credential file /srv/docker/apps/seafile/.secrets/seafile_db_pwd.txt}"
- name: seafile_db
container: seafile-db
username: seafile
password: "{credential file /srv/docker/apps/seafile/.secrets/seafile_db_pwd.txt}"
- name: seahub_db
container: seafile-db
username: seafile
password: "{credential file /srv/docker/apps/seafile/.secrets/seafile_db_pwd.txt}"

All three are required: seafile_db holds the file metadata, ccnet_db the users and groups, seahub_db the web layer with shares and links. Any one of them missing produces a restore that starts and is unusable.

Restore order: the three databases, then the file store, then the application.

  1. Read the release notes. Seafile’s major versions carry database migrations.

  2. Back up all three databases and volumes/seafile-data together.

  3. Raise the image variables in .env, then:

    Terminal window
    docker compose pull && docker compose up -d
    docker compose logs seafile --follow
  4. Log in and sync one file. A migration that touched the metadata tables shows up in the client rather than in the interface.

“Email service is not properly configured” right after installing. The settings block is missing. Run installation step 5, then check with step 6.

Document editing, metadata or thumbnails are absent after a fresh install. Same cause, same fix.

A restored instance starts but shares and links are gone. Only some of the three databases came back. All three are required.

A component should be switched off. Remove its filename from COMPOSE_FILE in .env and run docker compose up -d again.