Docker Deployment
S4 provides Docker images for easy deployment. The project includes a Dockerfile for the server and docker-compose-single-node.yml, which runs the server together with the web admin console.
Single Container
Build
docker build -t s4-server .
Run (Basic)
docker run -d \
--name s4-server \
-p 9000:9000 \
-v s4-data:/data \
s4-server:latest
Run with Credentials
docker run -d \
--name s4-server \
-p 9000:9000 \
-v s4-data:/data \
-e S4_ACCESS_KEY_ID=myaccesskey \
-e S4_SECRET_ACCESS_KEY=mysecretkey \
s4-server:latest
Run with IAM
openssl rand -hex 32 > jwt_secret # once; keep it with your other secrets
docker run -d \
--name s4-server \
-p 9000:9000 \
-v s4-data:/data \
-e S4_ROOT_PASSWORD=password12345 \
-e S4_JWT_SECRET="$(cat jwt_secret)" \
s4-server:latest
Without S4_JWT_SECRET a single node signs admin tokens with a random secret generated at
every start, so admin sessions end on restart. Setting it keeps them, as long as the value stays
the same: generate it once, not in every docker run. A secret shorter than 32 bytes stops the
start — see Admin JWT Secret.
Run with TLS
docker run -d \
--name s4-server \
-p 9000:9000 \
-v s4-data:/data \
-v /path/to/certs:/certs:ro \
-e S4_TLS_CERT=/certs/cert.pem \
-e S4_TLS_KEY=/certs/key.pem \
s4-server:latest
Docker Compose (Full Stack)
docker-compose-single-node.yml builds the Community Edition server from the
repository and runs it together with the web admin console:
services:
s4core:
build:
context: .
dockerfile: Dockerfile
ports:
- "9000:9000"
environment:
- S4_BIND=0.0.0.0:9000
- S4_DATA_DIR=/data
- S4_ROOT_USERNAME=${S4_ROOT_USERNAME:-root}
- S4_ROOT_PASSWORD=${S4_ROOT_PASSWORD:-password12345}
- S4_ACCESS_KEY_ID=${S4_ACCESS_KEY_ID:-my-access-key-one}
- S4_SECRET_ACCESS_KEY=${S4_SECRET_ACCESS_KEY:-my-secret-key-one}
volumes:
- s4-data:/data
s4console:
image: s4core/s4console:latest
ports:
- "3000:3000"
environment:
# The console reaches the server by its compose service name.
- S4_BACKEND_URL=http://s4core:9000
depends_on:
s4core:
condition: service_healthy
volumes:
s4-data:
The excerpt leaves out the health check and restart policy. The root credentials
and the S3 keys are read from the shell, with the defaults above as placeholders:
set S4_ROOT_PASSWORD, S4_ACCESS_KEY_ID and S4_SECRET_ACCESS_KEY before
exposing the server.
Start
# Full stack: server + web console
docker compose -f docker-compose-single-node.yml up -d --build
# Server only
docker compose -f docker-compose-single-node.yml up -d --build s4core
Access Points
| Service | URL |
|---|---|
| S4 API | http://localhost:9000 |
| Web Console | http://localhost:3000 (log in as root / S4_ROOT_PASSWORD) |
The console needs IAM, which S4_ROOT_PASSWORD turns on.
Rebuild After Changes
docker compose -f docker-compose-single-node.yml up -d --build
Stop
docker compose -f docker-compose-single-node.yml down # keep the data volume
docker compose -f docker-compose-single-node.yml down -v # delete it as well
View Logs
# All services
docker compose -f docker-compose-single-node.yml logs -f
# Server only
docker compose -f docker-compose-single-node.yml logs -f s4core
Credentials from Docker Secrets
Every credential can be read from a file by appending _FILE to its variable
(S4_ROOT_USERNAME, S4_ROOT_PASSWORD, S4_ACCESS_KEY_ID, S4_SECRET_ACCESS_KEY,
S4_JWT_SECRET). Docker mounts each secret as a file under /run/secrets/, so the values stay out
of docker inspect, the process environment and the compose file. The rules are in the
configuration reference.
services:
s4core:
image: s4core/s4core:latest
ports:
- "9000:9000"
volumes:
- s4-data:/data
environment:
- S4_ROOT_USERNAME_FILE=/run/secrets/s4_user
- S4_ROOT_PASSWORD_FILE=/run/secrets/s4_pass
- S4_ACCESS_KEY_ID_FILE=/run/secrets/s4_access_key
- S4_SECRET_ACCESS_KEY_FILE=/run/secrets/s4_secret_key
- S4_JWT_SECRET_FILE=/run/secrets/s4_jwt_secret
secrets: [s4_user, s4_pass, s4_access_key, s4_secret_key, s4_jwt_secret]
secrets:
s4_user:
file: ./secrets/s4_user
s4_pass:
file: ./secrets/s4_pass
s4_access_key:
file: ./secrets/s4_access_key
s4_secret_key:
file: ./secrets/s4_secret_key
s4_jwt_secret:
file: ./secrets/s4_jwt_secret
volumes:
s4-data:
Create the files once, for example openssl rand -hex 32 > secrets/s4_jwt_secret. In Docker Swarm,
declare the secrets as external: true and create them with docker secret create. If a file is
missing or empty, or a variable is set together with its _FILE form, the server stops at startup
and names the variable.
The server runs as user s4 (uid 1000) inside the container. Docker Compose without Swarm
bind-mounts a secret file with its owner and mode from the host, so a file readable only by root
cannot be read and the start fails with Permission denied. Give the files to uid 1000:
sudo chown 1000 secrets/* && sudo chmod 400 secrets/*. Swarm mounts secrets readable by everyone in
the container, so there nothing needs to change.
Volume Management
S4 stores all data in the /data directory inside the container. Always use a Docker volume or bind mount to persist data.
# Named volume (recommended)
-v s4-data:/data
# Bind mount
-v /var/lib/s4:/data
The server runs as the unprivileged user s4 (uid 1000), so a bind-mounted
directory has to be writable by that uid: sudo chown 1000:1000 /var/lib/s4.
A directory Docker creates for a missing bind source belongs to root, and the
server cannot write to it.
Warning: Without a volume, all data is lost when the container is removed.
Cluster Deployment (Docker)
For deploying a multi-node S4 cluster with Docker Compose, see Cluster Deployment.
Resource Recommendations
| Workload | CPU | RAM | Storage |
|---|---|---|---|
| Development / Testing | 1 core | 512MB | 1GB |
| Small (< 1M objects) | 2 cores | 2GB | As needed |
| Medium (1-100M objects) | 4 cores | 8GB | As needed |
| Large (> 100M objects) | 8+ cores | 16GB+ | NVMe recommended for metadata |