mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-10-23 03:27:43 +09:00
Two independent bugs, both surfaced by issue #96. The image died on every run with "sqlite3.OperationalError: unable to open database file": config.py defaults DATABASE_URL to sqlite:////app/instance/serverkit.db, the Dockerfile never created /app/instance, and SQLAlchemy will not create a parent directory. Creating it before the USER switch also fixes named-volume ownership -- Docker seeds an empty volume from the image directory, permissions included, but creates it root-owned when the path is absent from the image. Separately, every docker-compose.yml service sat behind the legacy-frontend profile, so `docker compose up -d` started nothing at all while the README and install guide both gave that command as the Docker install. Following the profile hint yielded the legacy nginx container alone -- a static file server plus a proxy to a backend on the host -- serving an empty bind mount, which is the 403 the reporter saw. Default `up -d` now runs the all-in-one image on :5000 with a named volume; the legacy service stays profiled and says in a comment that it needs a host install. DATABASE_URL is pinned rather than interpolated. .env.example shipped a *relative* sqlite path, which resolves against the container WORKDIR instead of the volume and is destroyed by the next `compose down`; anyone who copied that .env would inherit the trap. SERVERKIT_DATABASE_URL is the documented override. Verified against a real build: boots healthy, serves the SPA, survives a container recreate on the volume with 143 tables intact, and comes up with no .env present at all. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
142 lines
5.8 KiB
YAML
142 lines
5.8 KiB
YAML
# ============================================
|
|
# ServerKit Docker Compose
|
|
# ============================================
|
|
# `docker compose up -d` runs the all-in-one panel image (./Dockerfile):
|
|
# gunicorn serves the API, the React SPA and the Socket.IO websockets from ONE
|
|
# container on port 5000. Reach it at http://localhost:5000.
|
|
#
|
|
# There is NO nginx inside the container, and the app never redirects to HTTPS.
|
|
# That makes this the topology to use when you already run your own reverse
|
|
# proxy (Caddy, Traefik, Nginx Proxy Manager, …) with your own CA — point it at
|
|
# port 5000 and set SERVERKIT_PUBLIC_URL + TRUST_PROXY_HEADERS below. See
|
|
# docs/INSTALLATION.md → "Running behind your own reverse proxy".
|
|
#
|
|
# ⚠ A containerised panel CANNOT manage its own host: no systemd, no host
|
|
# nginx, no host packages, so per-app vhosts and managed sites are out. It is
|
|
# the right shape for a demo, for driving OTHER servers through agents, and
|
|
# for a panel behind your own proxy. To manage THIS machine, use the host
|
|
# installer instead:
|
|
#
|
|
# curl -fsSL https://serverkit.ai/install.sh | sudo bash
|
|
#
|
|
# Before the first run:
|
|
# cp .env.example .env # then set SECRET_KEY / JWT_SECRET_KEY
|
|
# ============================================
|
|
|
|
services:
|
|
serverkit:
|
|
build:
|
|
context: .
|
|
dockerfile: Dockerfile
|
|
image: serverkit:local
|
|
# Prefer the published release image over building locally? Comment out the
|
|
# `build:` block above and use:
|
|
# image: jhd3197/serverkit:latest
|
|
container_name: serverkit
|
|
ports:
|
|
# Host port is overridable because 5000 collides with some AirPlay/dev
|
|
# setups; the container always listens on 5000.
|
|
- "${SERVERKIT_HTTP_PORT:-5000}:5000"
|
|
env_file:
|
|
# required:false so a missing .env warns instead of aborting. The panel
|
|
# does boot without one — but on config.py's built-in dev SECRET_KEY and
|
|
# JWT_SECRET_KEY, which are public knowledge. Fine for a five-minute look,
|
|
# never for anything reachable. `cp .env.example .env` and set both.
|
|
# (Needs Compose >= 2.24 for this long form.)
|
|
- path: .env
|
|
required: false
|
|
environment:
|
|
# Canonical database location: on the named volume, so `compose down`
|
|
# keeps your data.
|
|
#
|
|
# Deliberately NOT `${DATABASE_URL:-...}`. .env.example used to ship a
|
|
# *relative* `sqlite:///serverkit.db`, which resolves against the
|
|
# container WORKDIR (/app/backend) — off the volume, and silently
|
|
# destroyed by the next `compose down`. Anyone who copied that .env would
|
|
# inherit the trap. An `environment:` entry outranks `env_file:`, so this
|
|
# line neutralises it.
|
|
#
|
|
# To use PostgreSQL (or another absolute path), set SERVERKIT_DATABASE_URL
|
|
# in .env — DATABASE_URL there is ignored under Docker by design:
|
|
# SERVERKIT_DATABASE_URL=postgresql://user:pass@db:5432/serverkit
|
|
DATABASE_URL: ${SERVERKIT_DATABASE_URL:-sqlite:////app/instance/serverkit.db}
|
|
# Required when a reverse proxy terminates TLS in front of this container:
|
|
# the Socket.IO origin check rejects the browser's handshake without it.
|
|
# SERVERKIT_PUBLIC_URL=https://panel.example.com
|
|
#
|
|
# TRUST_PROXY_HEADERS makes ServerKit derive the real client IP from
|
|
# X-Forwarded-For (rate limits, login lockout, audit logs) instead of
|
|
# logging every request as the proxy's address. Leave it off unless a
|
|
# proxy really is in front — on a directly-exposed port the header is
|
|
# client-forgeable. Both are read from .env.
|
|
FLASK_ENV: ${FLASK_ENV:-production}
|
|
volumes:
|
|
- serverkit-data:/app/instance
|
|
# Let the panel manage Docker containers on this host. ⚠ The docker socket
|
|
# is root-equivalent on the host — mounting it means a panel compromise is
|
|
# a host compromise. Opt in deliberately:
|
|
# - /var/run/docker.sock:/var/run/docker.sock
|
|
restart: unless-stopped
|
|
healthcheck:
|
|
test: ["CMD", "curl", "-f", "http://127.0.0.1:5000/api/v1/system/health"]
|
|
interval: 30s
|
|
timeout: 10s
|
|
retries: 3
|
|
start_period: 40s
|
|
logging:
|
|
driver: "json-file"
|
|
options:
|
|
max-size: "10m"
|
|
max-file: "3"
|
|
|
|
# --------------------------------------------
|
|
# Legacy: panel UI in its own nginx container.
|
|
# --------------------------------------------
|
|
# Only for a HOST install (`install.sh`), where the backend already runs on
|
|
# the host under systemd and `frontend/dist` has been built there — this
|
|
# service is a static-file server plus a proxy to the host, nothing more. On
|
|
# its own it serves an empty directory and nginx answers 403.
|
|
#
|
|
# It sits behind a profile, so plain `docker compose up -d` never starts it:
|
|
# docker compose --profile legacy-frontend up -d --build
|
|
#
|
|
# Then repoint the host nginx `location /` back at 127.0.0.1:3847.
|
|
frontend:
|
|
profiles: ["legacy-frontend"]
|
|
build:
|
|
context: ./frontend
|
|
dockerfile: Dockerfile
|
|
container_name: serverkit-frontend
|
|
ports:
|
|
- "3847:80"
|
|
# Resolve `backend` (used by frontend/nginx.conf) to the host, where the
|
|
# ServerKit backend listens on :5000 under systemd.
|
|
extra_hosts:
|
|
- "backend:host-gateway"
|
|
# install.sh builds the SPA to ./frontend/dist on the host; serve that build
|
|
# directly so a rebuild doesn't require rebuilding the image.
|
|
volumes:
|
|
- ./frontend/dist:/usr/share/nginx/html:ro
|
|
networks:
|
|
- serverkit-network
|
|
restart: unless-stopped
|
|
healthcheck:
|
|
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1/"]
|
|
interval: 30s
|
|
timeout: 10s
|
|
retries: 3
|
|
logging:
|
|
driver: "json-file"
|
|
options:
|
|
max-size: "10m"
|
|
max-file: "3"
|
|
|
|
volumes:
|
|
serverkit-data:
|
|
name: serverkit-data
|
|
|
|
networks:
|
|
serverkit-network:
|
|
driver: bridge
|
|
name: serverkit-network
|