Files
artcraft-services/build/selfhost/docker-compose.yml
Navaras P 25f38b79b8 Add docker-compose setup for self-hosting the backend (#68)
Closes storytold/artcraft#1301

Adds `build/selfhost/`, a Docker Compose setup that runs
`storyteller-web` with MySQL 8.4, Redis, Elasticsearch and SeaweedFS
(S3-compatible storage).

- A `migrate` service applies `_database/sql/migrations` in order and
records them in `__diesel_schema_migrations`, so reruns only apply new
migrations.
- `storage-init` creates the three buckets the server expects.
- The server image is built from `build/service_cpu.Dockerfile`.
- `.env.example` lists every variable the server requires at startup,
with placeholders for third-party providers.

SeaweedFS is used because MinIO no longer publishes Docker images.

**Tested locally (Docker Desktop on Windows):** image build, all 285
migrations (plus a rerun), `GET /_status` healthy, model listing,
account creation and login, and an image upload stored in the bucket.

**Not included:** background workers, Elasticsearch index setup.
2026-10-11 18:51:50 -04:00

164 lines
5.0 KiB
YAML

# Self-hosted ArtCraft backend (storyteller-web) with its dependencies.
#
# Usage (from this directory):
# cp .env.example .env
# docker compose up --build
#
# See README.md in this directory for details.
name: artcraft-selfhost
services:
mysql:
image: mysql:8.4
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: storyteller
MYSQL_USER: storyteller
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
volumes:
- mysql-data:/var/lib/mysql
healthcheck:
# NB: Use TCP (127.0.0.1), not the socket, so the check fails while the image's
# temporary init server (which has networking disabled) is still running.
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-p${MYSQL_ROOT_PASSWORD}", "--silent"]
interval: 5s
timeout: 5s
retries: 30
# Applies the SQL migrations in `_database/sql/migrations` in order.
# Applied versions are recorded in `__diesel_schema_migrations` (the same table
# the diesel CLI uses), so re-running is safe and only applies new migrations.
migrate:
image: mysql:8.4
depends_on:
mysql:
condition: service_healthy
environment:
MYSQL_PWD: ${MYSQL_ROOT_PASSWORD}
volumes:
- ../../_database/sql/migrations:/migrations:ro
entrypoint: ["bash", "-c"]
command:
- |
set -euo pipefail
run_sql() { mysql -h mysql -u root storyteller "$$@"; }
run_sql -e "CREATE TABLE IF NOT EXISTS __diesel_schema_migrations (
version VARCHAR(50) PRIMARY KEY NOT NULL,
run_on TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP)"
for dir in /migrations/*/; do
name=$$(basename "$$dir")
version=$$(echo "$${name%%_*}" | tr -d '-')
if [ -n "$$(run_sql -N -e "SELECT 1 FROM __diesel_schema_migrations WHERE version = '$$version'")" ]; then
continue
fi
echo "Applying migration $$name"
run_sql < "$$dir/up.sql"
run_sql -e "INSERT INTO __diesel_schema_migrations (version) VALUES ('$$version')"
done
echo "Migrations complete."
redis:
image: redis:7
restart: unless-stopped
volumes:
- redis-data:/data
elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:8.15.3
restart: unless-stopped
environment:
discovery.type: single-node
xpack.security.enabled: "false"
ES_JAVA_OPTS: -Xms512m -Xmx512m
volumes:
- elasticsearch-data:/usr/share/elasticsearch/data
healthcheck:
test: ["CMD-SHELL", "curl -fs http://localhost:9200/_cluster/health || exit 1"]
interval: 10s
timeout: 5s
retries: 30
# S3-compatible object storage (SeaweedFS) for uploaded and generated media.
seaweedfs:
image: chrislusf/seaweedfs:3.97
restart: unless-stopped
command: server -dir=/data -s3 -s3.port=8333 -s3.config=/etc/seaweedfs/s3.json
configs:
- source: seaweedfs-s3
target: /etc/seaweedfs/s3.json
volumes:
- seaweedfs-data:/data
healthcheck:
test: ["CMD-SHELL", "wget -q -O /dev/null http://127.0.0.1:8333/healthz || exit 1"]
interval: 5s
timeout: 5s
retries: 30
# Creates the buckets storyteller-web expects.
storage-init:
image: chrislusf/seaweedfs:3.97
depends_on:
seaweedfs:
condition: service_healthy
entrypoint: ["sh", "-c"]
command:
- |
set -e
for bucket in artcraft-private artcraft-public artcraft-public-gc; do
echo "s3.bucket.create -name $$bucket" | weed shell -master=seaweedfs:9333
done
storyteller-web:
build:
context: ../..
dockerfile: build/service_cpu.Dockerfile
image: artcraft-services:local
restart: unless-stopped
depends_on:
migrate:
condition: service_completed_successfully
storage-init:
condition: service_completed_successfully
redis:
condition: service_started
elasticsearch:
condition: service_healthy
env_file: .env
environment:
SERVER_ENVIRONMENT: development
BIND_ADDRESS: 0.0.0.0:12345
MYSQL_URL: mysql://storyteller:${MYSQL_PASSWORD}@mysql:3306/storyteller
REDIS_0_URL: redis://redis:6379/0
ELASTICSEARCH_URL: http://elasticsearch:9200
S3_COMPATIBLE_ENDPOINT_URL: http://seaweedfs:8333
ACCESS_KEY: ${S3_ACCESS_KEY}
SECRET_KEY: ${S3_SECRET_KEY}
REGION_NAME: us-east-1
W2L_PRIVATE_DOWNLOAD_BUCKET_NAME: artcraft-private
W2L_PUBLIC_DOWNLOAD_BUCKET_NAME: artcraft-public
GC_ENABLED_PUBLIC_BUCKET_NAME: artcraft-public-gc
AUDIO_UPLOADS_BUCKET_ROOT: /user_uploaded_audio
ports:
- "12345:12345"
configs:
seaweedfs-s3:
content: |
{
"identities": [
{
"name": "artcraft",
"credentials": [{ "accessKey": "${S3_ACCESS_KEY}", "secretKey": "${S3_SECRET_KEY}" }],
"actions": ["Admin", "Read", "Write", "List", "Tagging"]
}
]
}
volumes:
mysql-data:
redis-data:
elasticsearch-data:
seaweedfs-data: