Description
We’re currently trying to deploy SeaTable Enterprise (v6.0.6) using Dokploy with Docker Compose, and we’ve been running into a persistent issue related to the license file initialization and the /shared volume mapping.
The deployment itself works up to the point where SeaTable starts and wants the license key to login — but at that moment, something goes wrong with how the license file is handled.
When the container is deployed without a pre-existing seatable-license.txt file, SeaTable automatically creates a directory named seatable-license.txt inside the shared and opt data paths.
From that point onward, it’s impossible to properly mount or overwrite the license file because Docker treats the existing directory as a conflict.
We have tried deploying SeaTable in several different ways, but each approach leads to the same issue — the seatable-license.txt either gets created as a directory or SeaTable fails to recognize it properly.
Case 1 – No license file existed before deployment
When SeaTable is deployed for the first time and no seatable-license.txt file is present, the system automatically creates a directory named seatable-license.txt inside the /shared/seatable path.
Once that happens, any later attempt to add or mount a proper license file fails, because Docker cannot overwrite a directory with a file.
Case 2 – License volume temporarily disabled (commented out)
We also tried commenting out the volume mount for the license file during the first build, so that we could manually create the license file and then reload the container.
However, after reloading, SeaTable expects a directory at that same path instead of the file, which results in a startup error because it cannot find the directory it’s looking for.
Case 3 – Using an Init container to create the license automatically
Our current setup uses an init container (seatable-init-license) that generates the seatable-license.txt during the deployment process.
Even in this setup, SeaTable either converts the file into a directory at startup or fails with an error related to the license path — the result is always the same: the license is not recognized and the container stops during initialization.
In all scenarios, the underlying issue seems to be tied to how SeaTable handles the /shared/seatable/seatable-license.txt path during its first run — it either assumes it should create the directory structure from scratch, or it overwrites an existing file with a directory placeholder.
Logs
seatable-init-license:
2025-10-31T16:12:48.712Z /sbin/my_init:25: SyntaxWarning: invalid escape sequence ‘\\W’
2025-10-31T16:12:48.712Z SHENV_NAME_WHITELIST_REGEX = re.compile(‘\\W’)
2025-10-31T16:12:48.713Z /sbin/my_init:96: SyntaxWarning: invalid escape sequence ‘\\Z’
2025-10-31T16:12:48.713Z value = re.sub(‘\\n\\Z’, ‘’, f.read())
2025-10-31T16:12:48.777Z \*\*\* Running /etc/my_init.d/01_init.sh…
2025-10-31T16:12:48.811Z \*\*\* Booting runit daemon…
2025-10-31T16:12:48.812Z \*\*\* Runit started as PID 16
2025-10-31T16:12:48.822Z 2025-10-31 17:12:48 Conf exists
2025-10-31T16:12:48.857Z 2025-10-31 17:12:48 Nginx ready
2025-10-31T16:12:48.874Z 2025-10-31 17:12:48 Updating CA certificates…
2025-10-31T16:12:55.390Z 2025-10-31 17:12:55 Start server
2025-10-31T16:12:48.812Z \*\*\* Running /templates/enterpoint.sh…
2025-10-31T16:12:55.397Z cat: /shared/seatable/seatable-license.txt: Is a directory
2025-10-31T16:12:55.402Z cat: /shared/seatable/seatable-license.txt: Is a directory
2025-10-31T16:12:59.017Z SeaTable started
2025-10-31T16:12:59.025Z 2025-10-31 17:12:59 For more startup information, please check the /opt/seatable/logs/init.log
2025-10-31T16:12:59.029Z 2025-10-31 17:12:59 This is an idle script (infinite loop) to keep the container running.
seatable-server:
2025-10-31T16:12:48.712Z /sbin/my_init:25: SyntaxWarning: invalid escape sequence ‘\\W’
2025-10-31T16:12:48.712Z SHENV_NAME_WHITELIST_REGEX = re.compile(‘\\W’)
2025-10-31T16:12:48.713Z /sbin/my_init:96: SyntaxWarning: invalid escape sequence ‘\\Z’
2025-10-31T16:12:48.713Z value = re.sub(‘\\n\\Z’, ‘’, f.read())
2025-10-31T16:12:48.777Z \*\*\* Running /etc/my_init.d/01_init.sh…
2025-10-31T16:12:48.811Z \*\*\* Booting runit daemon…
2025-10-31T16:12:48.812Z \*\*\* Runit started as PID 16
2025-10-31T16:12:48.812Z \*\*\* Running /templates/enterpoint.sh…
2025-10-31T16:12:48.822Z 2025-10-31 17:12:48 Conf exists
2025-10-31T16:12:48.857Z 2025-10-31 17:12:48 Nginx ready
2025-10-31T16:12:48.874Z 2025-10-31 17:12:48 Updating CA certificates…
2025-10-31T16:12:55.390Z 2025-10-31 17:12:55 Start server
2025-10-31T16:12:59.017Z SeaTable started
2025-10-31T16:12:59.025Z 2025-10-31 17:12:59 For more startup information, please check the /opt/seatable/logs/init.log
2025-10-31T16:12:59.029Z 2025-10-31 17:12:59 This is an idle script (infinite loop) to keep the container running.
2025-10-31T16:12:55.397Z cat: /shared/seatable/seatable-license.txt: Is a directory
2025-10-31T16:12:55.402Z cat: /shared/seatable/seatable-license.txt: Is a directory
Docker Compose:
services:
# License file initializer - runs once to create the license file
seatable-init-license:
image: curlimages/curl:latest
container_name: seatable-init-license
restart: "no"
volumes:
- /opt/seatable
environment:
SEATABLE_LICENSE_URL: ${SEATABLE_LICENSE_URL:-}
SEATABLE_LICENSE_CONTENT: ${SEATABLE_LICENSE_CONTENT:-}
entrypoint: sh
command: |
-c '
echo "[INFO] Initializing license file..."
mkdir -p /shared/seatable
if [ -f "/shared/seatable/seatable-license.txt" ]; then
echo "[INFO] License file already exists"
elif [ -n "$${SEATABLE_LICENSE_URL}" ]; then
echo "[INFO] Downloading license file from URL..."
if curl -f -L -o /shared/seatable/seatable-license.txt "$${SEATABLE_LICENSE_URL}"; then
echo "[INFO] License file downloaded successfully"
else
echo "[ERROR] Failed to download license from URL: $${SEATABLE_LICENSE_URL}"
exit 1
fi
elif [ -n "$${SEATABLE_LICENSE_CONTENT}" ]; then
echo "[INFO] Creating license file from environment variable..."
echo "$${SEATABLE_LICENSE_CONTENT}" > /shared/seatable/seatable-license.txt
echo "[INFO] License file created from content"
else
echo "[INFO] Creating placeholder license file..."
cat > /shared/seatable/seatable-license.txt << "EOF"
# SeaTable License File
# Please add your license content here
# You can either:
# 1. Set the SEATABLE_LICENSE_URL environment variable with your license download URL
# 2. Set the SEATABLE_LICENSE_CONTENT environment variable with the license content
# 3. Edit this file directly after deployment
# 4. Use docker exec to add your license content
EOF
echo "[INFO] Placeholder license file created"
fi
chmod 644 /shared/seatable/seatable-license.txt
echo "[INFO] License initialization complete"
'
seatable-server:
image: seatable/seatable-enterprise:6.0.6
container_name: seatable-server
restart: unless-stopped
depends_on:
seatable-init-license:
condition: service_completed_successfully
mariadb:
condition: service_healthy
redis:
condition: service_healthy
environment:
# DB / Redis
SEATABLE_MYSQL_DB_HOST: ${MARIADB_HOST:-mariadb}
SEATABLE_MYSQL_DB_PORT: ${MARIADB_PORT:-3306}
SEATABLE_MYSQL_DB_USER: root
SEATABLE_MYSQL_DB_PASSWORD: ${MARIADB_PASSWORD}
INIT_SEATABLE_MYSQL_ROOT_PASSWORD: ${MARIADB_ROOT_PASSWORD:-${MARIADB_PASSWORD}}
SEATABLE_MYSQL_DB_DTABLE_DB_NAME: dtable_db
SEATABLE_MYSQL_DB_CCNET_DB_NAME: ccnet_db
SEATABLE_MYSQL_DB_SEAFILE_DB_NAME: seafile_db
REDIS_HOST: ${REDIS_HOST:-redis}
REDIS_PORT: ${REDIS_PORT:-6379}
REDIS_PASSWORD: ${REDIS_PASSWORD}
# Server
SEATABLE_SERVER_HOSTNAME: ${SEATABLE_SERVER_HOSTNAME}
SEATABLE_SERVER_PROTOCOL: http
SEATABLE_ADMIN_EMAIL: ${SEATABLE_ADMIN_EMAIL}
SEATABLE_ADMIN_PASSWORD: ${SEATABLE_ADMIN_PASSWORD}
TIME_ZONE: ${TIME_ZONE:-Europe/Berlin}
JWT_PRIVATE_KEY: ${JWT_PRIVATE_KEY}
# Optional
ENABLE_PYTHON_SCRIPT: "false"
SEATABLE_LOG_LEVEL: ERROR
volumes:
- /opt/seatable:/shared
# Traefik configuration
labels:
- traefik.enable=true
- traefik.http.routers.seatable.rule=Host(`${SEATABLE_SERVER_HOSTNAME}`)
- traefik.http.routers.seatable.entrypoints=websecure
- traefik.http.routers.seatable.tls=true
- traefik.http.services.seatable.loadbalancer.server.port=80
- traefik.docker.network=dokploy-network
networks:
- dokploy-network
- backend-seatable-net
healthcheck:
test: ["CMD-SHELL", "curl --fail http://localhost:8000 || exit 1"]
interval: 20s
retries: 3
start_period: 30s
timeout: 10s
mariadb:
image: mariadb:11.8.3-noble
container_name: mariadb
restart: unless-stopped
command: ["mariadbd","--innodb_snapshot_isolation=OFF"]
environment:
MYSQL_ROOT_PASSWORD: ${MARIADB_ROOT_PASSWORD:-${MARIADB_PASSWORD}}
MYSQL_LOG_CONSOLE: "true"
MARIADB_AUTO_UPGRADE: "1"
TZ: ${TIME_ZONE:-Europe/Berlin}
volumes:
- mariadb-data:/var/lib/mysql
healthcheck:
test: ["CMD","/usr/local/bin/healthcheck.sh","--connect","--mariadbupgrade","--innodb_initialized"]
interval: 20s
retries: 3
start_period: 30s
timeout: 10s
networks:
- backend-seatable-net
redis:
image: redis:8.2.2-bookworm
container_name: redis
restart: unless-stopped
environment:
REDIS_PASSWORD: ${REDIS_PASSWORD}
command: >
/bin/sh -c 'redis-server --requirepass "${REDIS_PASSWORD}"'
healthcheck:
test: ["CMD","redis-cli","ping"]
interval: 20s
retries: 3
timeout: 5s
networks:
- backend-seatable-net
volumes:
seatable-data:
name: seatable-data
mariadb-data:
name: mariadb-data
networks:
dokploy-network:
external: true
backend-seatable-net:
name: backend-seatable-net