Files
ServerKit/docker-compose.yml
Juan DenisandClaude Opus 5 de9523774c fix(docker): the panel image could not boot, and compose started nothing
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>
2026-08-13 23:05:55 -04:00

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