diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..50cc8107 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,20 @@ +.git +.github +.gitattributes +.gitignore +.gitlab-ci.yml +.venv +.pytest_cache +.coverage +**/__pycache__ +**/*.pyc +**/*.egg-info +*.log +docs/ +tests/ +ci/ +Dockerfile +Dockerfile.dev +docker-compose*.yml +/sims +/upload_folder diff --git a/.github/workflows/docker_image.yml b/.github/workflows/docker_image.yml new file mode 100644 index 00000000..4bf0d67f --- /dev/null +++ b/.github/workflows/docker_image.yml @@ -0,0 +1,115 @@ +name: Docker Image build and publish + +# Triggers and action summary: +# pull-request -> ignored in this GHA, linting and testing done elsewhere. +# push develop -> build and push image tagged : and :develop. +# push main -> build and push image tagged :latest. +# push tag -> build and push image tagged :. + +env: + CACHE_FROM: type=gha,scope=simdb-server + CACHE_TO: type=gha,mode=max,scope=simdb-server + SETUPTOOLS_SCM_OVERRIDES_FOR_IMAS_SIMDB: '{local_scheme = "no-local-version-strict"}' + +on: + push: + branches: [ "develop", "main" ] + tags: [ "*" ] + +# Serialize runs per ref, so that two merges in quick succession can't race to +# move the "develop" image tag backward. The running job is never cancelled; +# GitHub does however keep only the newest *pending* run per group, so a third +# queued run supersedes the second. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + +jobs: + build-and-publish: + name: Build and publish + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - name: Checkout code + uses: actions/checkout@v7 + with: + fetch-tags: true + fetch-depth: 0 + + - name: Get version + id: version + run: | + # A commit between tags describes as "--g", turned into the + # PEP 440 version ".dev+g"; a commit on a tag describes as + # just "" and is used as is. + VERSION=$(git describe --tags --always | sed -r 's/-([0-9]+)-g([0-9a-f]+)$/.dev\1+g\2/') + # A Docker tag allows only [A-Za-z0-9_.-], so drop the '+' local segment + # for the image tag. That is the version setuptools-scm bakes into the + # image (local_scheme = no-local-version-strict), so the tag and + # "simdb --version" agree. + echo "value=$VERSION" >> "$GITHUB_OUTPUT" + echo "tag_value=${VERSION%%+*}" >> "$GITHUB_OUTPUT" + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - name: Build build image + uses: docker/build-push-action@v7 + with: + target: build + load: true + tags: simdb-server:build + build-args: APP_VERSION=${{ steps.version.outputs.value }} + cache-from: ${{ env.CACHE_FROM }} + cache-to: ${{ env.CACHE_TO }} + + - name: Build service image + uses: docker/build-push-action@v7 + with: + target: service + load: true + tags: simdb-server:service + build-args: APP_VERSION=${{ steps.version.outputs.value }} + cache-from: ${{ env.CACHE_FROM }} + cache-to: ${{ env.CACHE_TO }} + + - name: Lowercase repo-owner + id: repo_owner + run: echo "value=${GITHUB_REPOSITORY_OWNER,,}" >> "$GITHUB_OUTPUT" + + - name: Login to GitHub Container Registry + uses: docker/login-action@v4 + with: + registry: ghcr.io + username: ${{ steps.repo_owner.outputs.value }} + password: ${{ secrets.GITHUB_TOKEN }} + + # Versioned tag: develop pushes and any tag push. main is excluded -- it + # follows develop and only publishes :latest. + - name: Tag and push service image with version tag + if: ${{ github.ref_type == 'tag' || github.ref_name == 'develop' }} + run: | + docker tag \ + simdb-server:service \ + ghcr.io/${{ steps.repo_owner.outputs.value }}/simdb-server:${{ steps.version.outputs.tag_value }} + docker push ghcr.io/${{ steps.repo_owner.outputs.value }}/simdb-server:${{ steps.version.outputs.tag_value }} + + # Run on develop pushes only + - name: Tag image :develop and publish + if: ${{ github.ref_type == 'branch' && github.ref_name == 'develop' }} + run: | + docker tag \ + simdb-server:service \ + ghcr.io/${{ steps.repo_owner.outputs.value }}/simdb-server:develop + docker push ghcr.io/${{ steps.repo_owner.outputs.value }}/simdb-server:develop + + # Run on main pushes only + - name: Tag image :latest and publish + if: ${{ github.ref_type == 'branch' && github.ref_name == 'main' }} + run: | + docker tag \ + simdb-server:service \ + ghcr.io/${{ steps.repo_owner.outputs.value }}/simdb-server:latest + docker push ghcr.io/${{ steps.repo_owner.outputs.value }}/simdb-server:latest diff --git a/.gitignore b/.gitignore index 64eae04e..ef3af8fb 100644 --- a/.gitignore +++ b/.gitignore @@ -11,8 +11,6 @@ environment.yml git_update runtests sdb -src/simdb/cli/runtests -src/simdb.egg-info test.yml test4_manifest.yml test7_manifest.yml @@ -31,3 +29,5 @@ src/simdb/_version.py *.egg-info *.egg *.whl +/sims +/upload_folder diff --git a/Dockerfile b/Dockerfile index 7f7aeb3f..17924169 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,10 @@ -ARG PYVER=3.12 -FROM ghcr.io/astral-sh/uv:python${PYVER}-trixie-slim +# Build stage: Install dependencies and prepare the application +FROM ghcr.io/astral-sh/uv:0.12.17-python3.12-trixie-slim@sha256:9a59bb7206905ccaae4f7dab222fbac47c125a21e5fc16f43f427cd6c940ade3 \ + AS build -ENV UV_NO_DEV=1 -ENV SETUPTOOLS_SCM_PRETEND_VERSION=0.0.0 +ENV UV_LINK_MODE=copy \ + UV_COMPILE_BYTECODE=1 \ + PYTHONUNBUFFERED=1 WORKDIR /app @@ -14,11 +16,67 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ libmagic1 \ && rm -rf /var/lib/apt/lists/* -COPY uv.lock pyproject.toml alembic.ini ./ +# Install dependencies in their own layer, cached independently. +COPY uv.lock pyproject.toml ./ +RUN uv sync --locked --no-dev --no-install-project --no-build --extra all + +ARG APP_VERSION=0.0.0 + +# Add the project source and finish the sync. +ENV SETUPTOOLS_SCM_PRETEND_VERSION_FOR_IMAS_SIMDB="${APP_VERSION}" +COPY alembic.ini ./ +COPY docker/gunicorn.conf.py ./docker/gunicorn.conf.py COPY src/ ./src/ -RUN uv sync --locked --extra all +RUN uv sync --locked --no-dev --extra all + +ENV SIMDB_SITE_CONFIG_PATH=/app/config/simdb.cfg + +# Runtime stage: Minimal image with only runtime dependencies +FROM ghcr.io/astral-sh/uv:0.12.17-python3.12-trixie-slim@sha256:9a59bb7206905ccaae4f7dab222fbac47c125a21e5fc16f43f427cd6c940ade3 \ + AS service + +ARG APP_UID=1000 +ARG APP_GID=1000 + +ENV UV_LINK_MODE=copy \ + UV_COMPILE_BYTECODE=1 \ + PYTHONUNBUFFERED=1 + +WORKDIR /app + +# Install only runtime dependencies (no build-essential, no *-dev variants, etc.) +RUN apt-get update && apt-get install -y --no-install-recommends \ + libpq5 \ + libldap2 \ + libsasl2-2 \ + libmagic1 \ + && rm -rf /var/lib/apt/lists/* + +RUN groupadd --gid ${APP_GID} simdb \ + && useradd --uid ${APP_UID} --gid ${APP_GID} --create-home --shell /usr/sbin/nologin simdb \ + && mkdir -p /data/simdb/simulations /home/simdb/.gunicorn \ + && chown -R simdb:simdb /data/simdb /home/simdb /app + +# Copy the prepared application and dependencies from build stage +COPY --from=build --chown=simdb:simdb /app/.venv /app/.venv +COPY --from=build --chown=simdb:simdb /app/alembic.ini ./ +COPY --from=build --chown=simdb:simdb /app/docker/gunicorn.conf.py ./docker/gunicorn.conf.py +COPY --from=build --chown=simdb:simdb /app/src/ ./src/ + +ARG APP_VERSION=0.0.0 + +LABEL org.opencontainers.image.title="SimDB" \ + org.opencontainers.image.description="SimDB Server — ITER simulation management tool" \ + org.opencontainers.image.source="https://github.com/iterorganization/SimDB" \ + org.opencontainers.image.licenses="LGPL-3.0-only" \ + org.opencontainers.image.version="${APP_VERSION}" \ + io.simdb.component="server" ENV SIMDB_SITE_CONFIG_PATH=/app/config/simdb.cfg -CMD ["uv", "run", "simdb_server"] +USER simdb + +EXPOSE 5000 +# Run under Gunicorn rather than the Werkzeug dev server +CMD ["uv", "run", "gunicorn", "--config=/app/docker/gunicorn.conf.py", "simdb.remote.wsgi:app"] diff --git a/Dockerfile.dev b/Dockerfile.dev index 4b37d3fd..f381390a 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -1,6 +1,27 @@ ARG PYVER=3.12 FROM ghcr.io/astral-sh/uv:python${PYVER}-trixie-slim +ENV UV_LINK_MODE=copy \ + PYTHONUNBUFFERED=1 + +WORKDIR /app + +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + libpq-dev \ + libldap2-dev \ + libsasl2-dev \ + libmagic1 \ + && rm -rf /var/lib/apt/lists/* + +# Install dependencies in their own layer, cached independently. +COPY uv.lock pyproject.toml ./ +RUN uv sync --locked --no-dev --no-install-project --no-build --extra all + +# Add the project source and finish the sync. +ARG APP_VERSION=0.0.0 +ENV SETUPTOOLS_SCM_PRETEND_VERSION_FOR_IMAS_SIMDB="${APP_VERSION}" + ARG SIMDB_GIT_BRANCH=unknown ARG SIMDB_GIT_COMMIT=unknown ARG SIMDB_DEPLOYMENT=local @@ -9,26 +30,17 @@ LABEL org.simdb.deployment="${SIMDB_DEPLOYMENT}" \ org.simdb.git.branch="${SIMDB_GIT_BRANCH}" \ org.simdb.git.commit="${SIMDB_GIT_COMMIT}" -ENV UV_NO_DEV=1 -ENV SETUPTOOLS_SCM_PRETEND_VERSION=0.0.0 + ENV SIMDB_DEPLOYMENT="${SIMDB_DEPLOYMENT}" ENV SIMDB_GIT_BRANCH="${SIMDB_GIT_BRANCH}" ENV SIMDB_GIT_COMMIT="${SIMDB_GIT_COMMIT}" -WORKDIR /app - -RUN apt-get update && apt-get install -y --no-install-recommends \ - build-essential \ - libpq-dev \ - libldap2-dev \ - libsasl2-dev \ - libmagic1 \ - && rm -rf /var/lib/apt/lists/* - -COPY uv.lock pyproject.toml alembic.ini ./ +COPY alembic.ini ./ COPY src/ ./src/ -RUN uv sync --locked --extra all +RUN uv sync --locked --no-dev --extra all ENV SIMDB_SITE_CONFIG_PATH=/app/config/simdb.cfg +EXPOSE 5000 + CMD ["uv", "run", "simdb_server"] diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..403fe3bc --- /dev/null +++ b/Makefile @@ -0,0 +1,161 @@ +# Build and deploy the SimDB server as a systemd service. +# +# Two ways it is used: +# * `make service` / `make up` — build / run the compose stack locally. +# * `sudo make systemd-install` — install the compose files, config and unit +# file onto the host, then drive the service with `systemctl`. +# +# Variables can be overridden on the command line, e.g.: +# * Stage an installation under a prefix instead of installing under / +# (useful for packaging): make systemd-install DESTDIR=/tmp/simdb-staging +# * Run the locally built image (make service) instead of the published one +# (ghcr.io/iterorganization/simdb-server:latest): +# make up SIMDB_SERVER_IMAGE=simdb-server SIMDB_SERVER_TAG=service + +SHELL := /bin/sh + +# APP_VERSION is baked into the image; derive a PEP 440 version from git. +VERSION := $(shell git describe --tags --long --always 2>/dev/null | sed -E 's/-([0-9]+)/.dev\1/;s/-/+/' || echo 0.0.0) + +PROJECT_NAME ?= simdb-server +COMPOSE_PROJECT_NAME ?= $(PROJECT_NAME) + +# Base compose file first, then the systemd override on top. +COMPOSE_FILE ?= docker-compose.yml:docker-compose.systemd.yml + +DOCKER_CMD ?= docker +DOCKER_BUILD ?= $(DOCKER_CMD) build --build-arg APP_VERSION="$(VERSION)" +DOCKER_COMPOSE ?= APP_VERSION="$(VERSION)" COMPOSE_FILE="$(COMPOSE_FILE)" $(DOCKER_CMD) compose + +SERVICE_IMAGE := simdb-server:service + +# Destinations used by systemd-install/systemd-uninstall (DESTDIR for packaging). +package_optdir ?= /opt/$(PROJECT_NAME) +package_etcdir ?= /etc/$(PROJECT_NAME) +systemd_unitdir ?= /etc/systemd/system + +.DEFAULT_GOAL := service + +.PHONY: \ + down \ + help \ + list \ + list-all \ + logs-f \ + service \ + shell \ + systemd-daemon-reload \ + systemd-disable \ + systemd-enable \ + systemd-install \ + systemd-installdirs \ + systemd-start \ + systemd-status \ + systemd-stop \ + systemd-uninstall \ + up + +help: + @echo "Build and run:" + @echo " make service Build and tag the service image ($(SERVICE_IMAGE))" + @echo " make up Start the stack from the configured image" + @echo " make down Stop the stack" + @echo "" + @echo " make list List the web container for this compose project" + @echo " make list-all List all containers for this compose project" + @echo " make logs-f Follow the web container's logs" + @echo " make shell Open a shell in the web container" + @echo "" + @echo " SIMDB_SERVER_IMAGE=simdb-server SIMDB_SERVER_TAG=service make up" + @echo " Run the image built by 'make service' instead of the published one" + @echo "" + @echo "Systemd (run with sudo; see docs/how-to/operate-server/install-server.md):" + @echo " sudo make systemd-install Install files under $(package_optdir) and $(package_etcdir)" + @echo " sudo make systemd-enable systemctl daemon-reload && systemctl enable $(PROJECT_NAME)" + @echo " sudo make systemd-start systemctl start $(PROJECT_NAME)" + @echo " sudo make systemd-status systemctl status $(PROJECT_NAME)" + @echo " sudo make systemd-stop systemctl stop $(PROJECT_NAME)" + @echo " sudo make systemd-disable systemctl stop && systemctl disable $(PROJECT_NAME)" + @echo " sudo make systemd-uninstall Remove everything systemd-install created" + +service: + $(DOCKER_BUILD) --target service -t $(SERVICE_IMAGE) . + +# --- compose --------------------------------------------------------------- + +up: + $(DOCKER_COMPOSE) --profile with_workers up -d --no-build + +down: + $(DOCKER_COMPOSE) --profile with_workers down + +list: + $(DOCKER_CMD) ps \ + --filter "label=com.docker.compose.project=$(COMPOSE_PROJECT_NAME)" \ + --filter "label=com.docker.compose.service=web" + +list-all: + $(DOCKER_CMD) ps \ + --filter "label=com.docker.compose.project=$(COMPOSE_PROJECT_NAME)" + +logs-f: + $(DOCKER_COMPOSE) logs -f web + +shell: + $(DOCKER_COMPOSE) exec web sh + +# --- systemd integration (run with sudo) ----------------------------------- +# +# The service unit runs `docker compose` from $(package_optdir), which is why +# the compose files, the config and validation data are copied there rather +# than bind-mounted from the source tree. + +systemd-installdirs: + mkdir -p \ + $(DESTDIR)$(package_optdir)/config \ + $(DESTDIR)$(package_optdir)/validation \ + $(DESTDIR)$(package_optdir)/upload_folder \ + $(DESTDIR)$(package_optdir)/tmp/partition_data \ + $(DESTDIR)$(package_etcdir) \ + $(DESTDIR)$(systemd_unitdir) + +systemd-install: systemd-installdirs + install -m 644 \ + docker-compose.yml \ + docker-compose.systemd.yml \ + $(DESTDIR)$(package_optdir) + install -m 644 \ + config/simdb.cfg \ + $(DESTDIR)$(package_optdir)/config + install -m 644 \ + validation/iter_scenarios_validation.yaml \ + $(DESTDIR)$(package_optdir)/validation + install -m 644 \ + docker/scripts/simdb-server.service \ + $(DESTDIR)$(systemd_unitdir)/$(PROJECT_NAME).service + install -m 644 \ + docker/scripts/simdb-server.env.example \ + "$(DESTDIR)$(package_etcdir)/$(PROJECT_NAME).env.example" + @if [ ! -f "$(DESTDIR)$(package_etcdir)/$(PROJECT_NAME).env" ]; then \ + install -m 644 docker/scripts/simdb-server.env.example \ + "$(DESTDIR)$(package_etcdir)/$(PROJECT_NAME).env"; \ + fi + +systemd-uninstall: systemd-disable + @printf 'Remove $(DESTDIR)$(package_optdir) and $(DESTDIR)$(package_etcdir)? [y/N] '; \ + read ans; case "$$ans" in [yY]*) ;; *) echo "aborted"; exit 1;; esac + -rm -f "$(DESTDIR)$(systemd_unitdir)/$(PROJECT_NAME).service" + -rm -rf "$(DESTDIR)$(package_etcdir)" + -rm -rf "$(DESTDIR)$(package_optdir)" + +systemd-daemon-reload: + systemctl daemon-reload + +systemd-enable: systemd-daemon-reload + systemctl enable $(PROJECT_NAME) + +systemd-disable: systemd-stop + systemctl disable $(PROJECT_NAME) + +systemd-start systemd-status systemd-stop: + -systemctl $(patsubst systemd-%,%,$@) $(PROJECT_NAME) diff --git a/config/simdb.cfg b/config/simdb.cfg index 4a6816f5..4d77b30b 100644 --- a/config/simdb.cfg +++ b/config/simdb.cfg @@ -2,7 +2,11 @@ flask_env = development debug = True testing = True -secret_key = CHANGE ME +secret_key = CHANGE_ME + +[cache] +type = SimpleCache +default_timeout = 300 [authentication] type=none @@ -12,7 +16,7 @@ upload_folder = /data/simdb/simulations port = 5000 ssl_enabled = False authentication_type = None -admin_password=CHANGE_ME +admin_password = CHANGE_ME imas_remote_host = localhost [database] diff --git a/dev_requirements.txt b/dev_requirements.txt index 031eeb99..17444928 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -15,7 +15,6 @@ distro==1.8.0 setuptools>=59 pytest-cov~=3.0 email-validator~=1.1 -flask-mail~=0.9.1 semantic-version>=2.8 click>=7.0,<=8.1.8 PyJWT>=2.3 diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index e09d481d..6849e27d 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -7,6 +7,7 @@ services: SIMDB_GIT_BRANCH: ${SIMDB_GIT_BRANCH:-unknown} SIMDB_GIT_COMMIT: ${SIMDB_GIT_COMMIT:-unknown} SIMDB_DEPLOYMENT: ${SIMDB_DEPLOYMENT:-local} + image: simdb-dev:${SIMDB_IMAGE_TAG:-local} volumes: - ./config:/app/config:ro environment: @@ -29,6 +30,7 @@ services: SIMDB_GIT_BRANCH: ${SIMDB_GIT_BRANCH:-unknown} SIMDB_GIT_COMMIT: ${SIMDB_GIT_COMMIT:-unknown} SIMDB_DEPLOYMENT: ${SIMDB_DEPLOYMENT:-local} + image: simdb-dev:${SIMDB_IMAGE_TAG:-local} environment: IMAS_LOCAL_HOSTS: localhost SIMDB_DEPLOYMENT: ${SIMDB_DEPLOYMENT:-local} @@ -78,7 +80,7 @@ services: org.simdb.git.branch: ${SIMDB_GIT_BRANCH:-unknown} org.simdb.git.commit: ${SIMDB_GIT_COMMIT:-unknown} volumes: - - ./deploy/nginx.dev.conf.template:/etc/nginx/templates/default.conf.template:ro + - ./docker/nginx.dev.conf.template:/etc/nginx/templates/default.conf.template:ro depends_on: web: condition: service_healthy @@ -105,6 +107,7 @@ services: SIMDB_GIT_BRANCH: ${SIMDB_GIT_BRANCH:-unknown} SIMDB_GIT_COMMIT: ${SIMDB_GIT_COMMIT:-unknown} SIMDB_DEPLOYMENT: ${SIMDB_DEPLOYMENT:-local} + image: simdb-dev:${SIMDB_IMAGE_TAG:-local} command: uv run simdb_worker environment: SIMDB_DEPLOYMENT: ${SIMDB_DEPLOYMENT:-local} @@ -134,6 +137,7 @@ services: SIMDB_GIT_BRANCH: ${SIMDB_GIT_BRANCH:-unknown} SIMDB_GIT_COMMIT: ${SIMDB_GIT_COMMIT:-unknown} SIMDB_DEPLOYMENT: ${SIMDB_DEPLOYMENT:-local} + image: simdb-dev:${SIMDB_IMAGE_TAG:-local} command: uv run simdb_beat environment: SIMDB_DEPLOYMENT: ${SIMDB_DEPLOYMENT:-local} @@ -168,6 +172,7 @@ services: - POSTGRES_USER=simdb - POSTGRES_PASSWORD=simdb - POSTGRES_DB=simdb + - POSTGRES_INITDB_ARGS=--locale-provider=icu --icu-locale=en_US.utf8 # workaround for alpine not having locale volumes: - postgres_data:/var/lib/postgresql/data healthcheck: diff --git a/docker-compose-pyver.yml b/docker-compose-pyver.yml deleted file mode 100644 index 63aa89ce..00000000 --- a/docker-compose-pyver.yml +++ /dev/null @@ -1,54 +0,0 @@ -# This docker-compose-pyver.yml extends the base docker-compose.yml -# to run three web services for different PYVER (3.11 and 3.13, 3.12 is the default) -# but they all use the same redis and postgres service. -# Run `docker compose -f docker-compose-workers.yml up` to start it up. -include: - - docker-compose.yml - -services: - migrations-311: - extends: - service: migrations - build: - args: - PYVER: "3.11" - - migrations-313: - extends: - service: migrations - build: - args: - PYVER: "3.13" - - web-311: - extends: - service: web - build: - args: - PYVER: "3.11" - ports: !override - - "5001:5000" - depends_on: !override - # !override is necessary to remove the original migrations from depends_on - redis: - condition: service_healthy - postgres: - condition: service_healthy - migrations-311: - condition: service_completed_successfully - - web-313: - extends: - service: web - build: - args: - PYVER: "3.13" - ports: !override - - "5003:5000" - depends_on: !override - redis: - condition: service_healthy - postgres: - condition: service_healthy - migrations-313: - condition: service_completed_successfully diff --git a/docker-compose.systemd.yml b/docker-compose.systemd.yml new file mode 100644 index 00000000..224be256 --- /dev/null +++ b/docker-compose.systemd.yml @@ -0,0 +1,23 @@ +# Systemd override for docker-compose.yml. +# +# Uses the GHCR-published image instead of a local build. +# +# Set SIMDB_SERVER_TAG in the environment file to pin a specific +# version. The default is the latest tag at the time of writing. +# +# Enable this override by including it in COMPOSE_FILE, +# after the original, e.g. +# COMPOSE_FILE="docker-compose.yml:docker-compose.systemd.yml" +# +services: + web: + # Systemd deployment should only manage a single container instance, docker requires + # container_name to be unique, so use it to ensure a fixed name. + container_name: ${SYSTEMD_CONTAINER_SIMDB:-simdb-server-systemd} + image: ${SIMDB_SERVER_IMAGE:-ghcr.io/iterorganization/simdb-server}:${SIMDB_SERVER_TAG:-latest} + logging: + driver: journald + options: + tag: simdb-server-systemd + postgres: + container_name: ${SYSTEMD_CONTAINER_POSTGRES:-simdb-postgres-systemd} diff --git a/docker-compose.yml b/docker-compose.yml index eff71ff3..bfbb7c65 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,19 +1,34 @@ services: migrations: - build: . + build: + context: . + args: + APP_VERSION: ${APP_VERSION:-0.0.0} + image: simdb:${SIMDB_IMAGE_TAG:-local} volumes: - ./config:/app/config:ro environment: DATABASE_URL: postgresql+psycopg2://simdb:simdb@postgres:5432/simdb depends_on: - - postgres + postgres: + condition: service_healthy command: uv run alembic upgrade head restart: "no" web: - build: . + build: + context: . + args: + APP_VERSION: ${APP_VERSION:-0.0.0} + image: simdb:${SIMDB_IMAGE_TAG:-local} + container_name: ${SIMDB_CONTAINER_NAME:-simdb-web} environment: IMAS_LOCAL_HOSTS: localhost + GUNICORN_BIND: ${GUNICORN_BIND:-0.0.0.0:5000} + GUNICORN_WORKERS: ${GUNICORN_WORKERS:-3} + GUNICORN_WORKER_CLASS: ${GUNICORN_WORKER_CLASS:-gthread} + GUNICORN_THREADS: ${GUNICORN_THREADS:-4} + GUNICORN_TIMEOUT: ${GUNICORN_TIMEOUT:-120} ports: - "5000:5000" volumes: @@ -33,7 +48,11 @@ services: worker: profiles: [with_workers] # docker compose --profile with_workers enables this service - build: . + build: + context: . + args: + APP_VERSION: ${APP_VERSION:-0.0.0} + image: simdb:${SIMDB_IMAGE_TAG:-local} command: uv run simdb_worker volumes: - ./config:/app/config:ro @@ -51,7 +70,11 @@ services: beat: profiles: [with_workers] # docker compose --profile with_workers enables this service - build: . + build: + context: . + args: + APP_VERSION: ${APP_VERSION:-0.0.0} + image: simdb:${SIMDB_IMAGE_TAG:-local} command: uv run simdb_beat volumes: - ./config:/app/config:ro @@ -65,7 +88,7 @@ services: restart: unless-stopped redis: - image: redis:8-alpine + image: redis:8.6.6-alpine3.23@sha256:23a604374ece8069b34131faac7a1f05ae73c64280e46af1de844f29af2846d4 volumes: - redis_data:/data healthcheck: @@ -76,18 +99,20 @@ services: restart: unless-stopped postgres: - image: postgres:16-alpine + image: postgres:16.15-alpine3.23@sha256:621a761097839bdb50207afd6b87a72f38e2d718dd46c3d744828d8917c4f1e0 environment: - POSTGRES_USER=simdb - POSTGRES_PASSWORD=simdb - POSTGRES_DB=simdb + - POSTGRES_INITDB_ARGS=--locale-provider=icu --icu-locale=en_US.utf8 # workaround for alpine not having locale volumes: - postgres_data:/var/lib/postgresql/data healthcheck: - test: ["CMD-SHELL", "pg_isready -U simdb"] - interval: 1s + test: ["CMD-SHELL", "pg_isready -U simdb -d simdb"] + interval: 2s timeout: 5s - retries: 5 + retries: 15 + start_period: 30s restart: unless-stopped volumes: diff --git a/docker/gunicorn.conf.py b/docker/gunicorn.conf.py new file mode 100644 index 00000000..fafe68ce --- /dev/null +++ b/docker/gunicorn.conf.py @@ -0,0 +1,11 @@ +import os + +# Binding: serve via TCP "0.0.0.0:5000" for Docker or network, +# or via unix socket "unix:/var/run/simdb.sock" for bare-metal and same host. +bind = os.environ.get("GUNICORN_BIND", "0.0.0.0:5000") +workers = int(os.environ.get("GUNICORN_WORKERS", "3")) +worker_class = os.environ.get("GUNICORN_WORKER_CLASS", "gthread") +threads = int(os.environ.get("GUNICORN_THREADS", "4")) +timeout = int(os.environ.get("GUNICORN_TIMEOUT", "120")) +accesslog = "-" +errorlog = "-" diff --git a/deploy/nginx.dev.conf.template b/docker/nginx.dev.conf.template similarity index 100% rename from deploy/nginx.dev.conf.template rename to docker/nginx.dev.conf.template diff --git a/docker/proxy_params b/docker/proxy_params deleted file mode 100644 index df75bc5d..00000000 --- a/docker/proxy_params +++ /dev/null @@ -1,4 +0,0 @@ -proxy_set_header Host $http_host; -proxy_set_header X-Real-IP $remote_addr; -proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; -proxy_set_header X-Forwarded-Proto $scheme; diff --git a/docker/scripts/postgres-backup b/docker/scripts/postgres-backup new file mode 100755 index 00000000..fb97bf00 --- /dev/null +++ b/docker/scripts/postgres-backup @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [[ $# -ne 1 ]]; then + echo "usage: $0 BACKUP_FILE" >&2 + exit 2 +fi + +backup_file="$1" +backup_dir="$(dirname "$backup_file")" +[[ -d "$backup_dir" ]] || { + echo "backup directory does not exist: $backup_dir" >&2 + exit 2 +} + +temporary_file="$(mktemp "${backup_file}.tmp.XXXXXX")" +trap 'rm -f "$temporary_file"' EXIT + +docker compose exec -T postgres \ + pg_dump \ + --username="${POSTGRES_USER:-simdb}" \ + --dbname="${POSTGRES_DB:-simdb}" \ + --format=custom \ + >"$temporary_file" + +mv "$temporary_file" "$backup_file" +trap - EXIT diff --git a/docker/scripts/postgres-restore b/docker/scripts/postgres-restore new file mode 100755 index 00000000..4bf3729b --- /dev/null +++ b/docker/scripts/postgres-restore @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [[ $# -ne 1 ]]; then + echo "usage: $0 BACKUP_FILE" >&2 + exit 2 +fi + +backup_file="$1" +[[ -f "$backup_file" ]] || { + echo "backup file does not exist: $backup_file" >&2 + exit 2 +} + +docker compose exec -T postgres \ + pg_restore \ + --username="${POSTGRES_USER:-simdb}" \ + --dbname="${POSTGRES_DB:-simdb}" \ + --clean \ + --if-exists \ + --no-owner \ + <"$backup_file" diff --git a/docker/scripts/simdb-server.env.example b/docker/scripts/simdb-server.env.example new file mode 100644 index 00000000..3025827a --- /dev/null +++ b/docker/scripts/simdb-server.env.example @@ -0,0 +1,27 @@ +## Environment file used by scripts/simdb-server.service. +## Everything here is commented out to show defaults. + +## Select compose file overrides. `docker compose` will use docker-compose.yml by default, +## unless COMPOSE_FILE or --file is set. +## +## The systemd variant (prefers online published image), default: +#COMPOSE_FILE=docker-compose.yml:docker-compose.systemd.yml + +## Optional: use a custom server container name (shows up in docker ps etc) +#SYSTEMD_CONTAINER_SIMDB=simdb-server-systemd + +## Choose image to use for the simdb-server container +#SIMDB_SERVER_TAG=latest +#SIMDB_SERVER_IMAGE=ghcr.io/iterorganization/simdb-server +# To run the locally built docker image (e.g. `make service`), use: +#SIMDB_SERVER_IMAGE=simdb-server +#SIMDB_SERVER_TAG=service + +# Binding: serve via TCP "0.0.0.0:5000" for Docker or network, +#GUNICORN_BIND=0.0.0.0:5000 + +# Some other GUNICORN flags +#GUNICORN_WORKERS=3 +#GUNICORN_WORKER_CLASS=gthread +#GUNICORN_THREADS=4 +#GUNICORN_TIMEOUT=120 diff --git a/docker/scripts/simdb-server.service b/docker/scripts/simdb-server.service new file mode 100644 index 00000000..22ea6d63 --- /dev/null +++ b/docker/scripts/simdb-server.service @@ -0,0 +1,54 @@ +# SimDB Server systemd service unit. + +[Unit] +Description=SimDB Server — ITER simulation management tool +Documentation=https://github.com/iterorganization/SimDB +Requires=docker.service +After=docker.service network-online.target +StartLimitIntervalSec=60 +StartLimitBurst=3 + +[Service] +Type=oneshot +RemainAfterExit=yes + +# --- paths ------------------------------------------------------------ +EnvironmentFile=/etc/simdb-server/simdb-server.env +WorkingDirectory=/opt/simdb-server + +# --- defaults (overridden by env file when set) ----------------------- +Environment="COMPOSE_FILE=docker-compose.yml:docker-compose.systemd.yml" +#Environment="SYSTEMD_CONTAINER_SIMDB=simdb-server-systemd" +#Environment="SIMDB_SERVER_TAG=latest" +#Environment="SIMDB_SERVER_IMAGE=ghcr.io/iterorganization/simdb-server" +#Environment="GUNICORN_BIND=0.0.0.0:5000" +#Environment="GUNICORN_WORKERS=3" +#Environment="GUNICORN_WORKER_CLASS=gthread" +#Environment="GUNICORN_THREADS=4" +#Environment="GUNICORN_TIMEOUT=120" + +# --- lifecycle -------------------------------------------------------- +ExecStartPre=-/usr/bin/docker compose --profile with_workers pull --ignore-pull-failures +ExecStart=/usr/bin/docker compose --profile with_workers up --detach --remove-orphans --no-build +ExecStop=/usr/bin/docker compose --profile with_workers down + +# --- restart behaviour ------------------------------------------------ +Restart=on-failure +RestartSec=10s + +# --- stop timeout ----------------------------------------------------- +TimeoutStopSec=30 + +# --- security hardening ----------------------------------------------- +NoNewPrivileges=yes +CapabilityBoundingSet=~CAP_SYS_ADMIN CAP_NET_ADMIN +ProtectSystem=strict +ProtectHome=yes +PrivateTmp=yes + +# --- logging ---------------------------------------------------------- +StandardOutput=journal +StandardError=journal + +[Install] +WantedBy=multi-user.target diff --git a/docker/simdb.nginx b/docker/simdb.nginx deleted file mode 100644 index a2de9f0d..00000000 --- a/docker/simdb.nginx +++ /dev/null @@ -1,9 +0,0 @@ -server { - listen 80; - server_name localhost; - - location / { - include proxy_params; - proxy_pass http://simdb:5000; - } -} diff --git a/docs/how-to/operate-server/enable-ssl.md b/docs/how-to/operate-server/enable-ssl.md index 7078c5be..aa114fdc 100644 --- a/docs/how-to/operate-server/enable-ssl.md +++ b/docs/how-to/operate-server/enable-ssl.md @@ -3,7 +3,12 @@ A production SimDB server must serve over HTTPS. There are two ways to enable SSL, depending on how you run the server. -## Option A: TLS at Nginx (recommended) +**For container deployments**, the sister project +[SimDB-Dashboard](https://github.com/iterorganization/SimDB-Dashboard) provides +a complete nginx configuration, including TLS, that proxies `/scenarios/api` to +the SimDB backend. This is the recommended approach for modern deployments. + +## Option A: TLS at Nginx When [running behind Nginx and Gunicorn](run-behind-nginx-gunicorn.md), let Nginx terminate TLS. Change `/etc/nginx/conf.d/simdb.conf` to listen on 443 and @@ -21,8 +26,16 @@ server { ssl_certificate /etc/pki/nginx/server.crt; ssl_certificate_key /etc/pki/nginx/private/server.key; - location / { - include proxy_params; + location /scenarios/api { + # Typical proxy params + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # Inform simdb backend to add this prefix in their responses + proxy_set_header X-Forwarded-Prefix /scenarios/api; + proxy_pass http://unix:/var/run/simdb.sock; } } diff --git a/docs/how-to/operate-server/install-server.md b/docs/how-to/operate-server/install-server.md index 96eb2fb4..828ef57a 100644 --- a/docs/how-to/operate-server/install-server.md +++ b/docs/how-to/operate-server/install-server.md @@ -78,9 +78,104 @@ the [Docker Compose deployment](run-with-docker.md) instead of installing by hand. ``` +## Deploy as a systemd service + +The `Makefile` at the repository root automates the usual Docker Compose +deployment as a systemd unit: it installs the Compose files, the server +configuration and validation data under `/opt/simdb-server`, writes the unit +file and environment file, and wraps the `systemctl` calls. Run it from the +repository root, through `sudo` for anything that touches the system: + +```bash +sudo make systemd-install # copy files into place +sudo make systemd-enable # daemon-reload and enable at boot +sudo make systemd-start # start the service +``` + +Day-to-day control: + +```bash +sudo make systemd-status +sudo make systemd-stop +sudo make systemd-disable # stop and disable at boot +sudo make systemd-uninstall # stop and remove installed files +``` + +`make help` prints the same list, and the Compose targets +(`up`, `down`, `logs-f`, `shell`, …) are available for running the stack without +installing a service. See +[Run with Docker Compose](run-with-docker.md) for what the stack contains. + +### What gets installed + +| Path | Contents | +| --- | --- | +| `/opt/simdb-server` | Compose files, `config/simdb.cfg`, and validation data | +| `/etc/simdb-server/simdb-server.env` | Environment file read by the unit (`docker/scripts/simdb-server.env.example` on first install) | +| `/etc/systemd/system/simdb-server.service` | The systemd unit | + +The unit starts the stack from the installed Compose files, so the source +checkout is no longer needed once `systemd-install` has run. Database and Redis +state persist in the Compose named volumes and survives an uninstall. + +### Configure + +Edit `/etc/simdb-server/simdb-server.env` before starting the service. It +selects the image and sets the Gunicorn runtime options; see +[docker/scripts/simdb-server.env.example](../../../docker/scripts/simdb-server.env.example) +for every variable, commented out with its default. + +By default the service runs the published image +(`ghcr.io/iterorganization/simdb-server:latest`). To run an image built from your +checkout instead, build it and point the environment file at it: + +```bash +make service +``` + +```bash +# /etc/simdb-server/simdb-server.env +SIMDB_SERVER_IMAGE=simdb-server +SIMDB_SERVER_TAG=service +``` + +Images are published to `ghcr.io/iterorganization/simdb-server` by the +[Docker Image build and publish](https://github.com/iterorganization/SimDB/blob/develop/.github/workflows/docker_image.yml) +workflow: + +| Tag | Published on | +| --- | --- | +| `latest` | every push to `main` | +| `develop` | every push to `develop` | +| `` | every push to `develop`, and every tag push | + +`` is the version reported by `simdb --version` inside that image: the +tag name itself for a release (for example `0.15.2`), or a development version +such as `0.15.2.dev319` for a build between releases. In production, pin it +rather than tracking a moving tag: + +```bash +# /etc/simdb-server/simdb-server.env +SIMDB_SERVER_TAG=0.15.2 +``` + +The server itself is configured through the installed +`/opt/simdb-server/config/simdb.cfg`, exactly as described in +[Run with Docker Compose](run-with-docker.md#configure). Installation paths and +the Compose project name can be overridden on the command line, for example +`sudo make systemd-install package_optdir=/srv/simdb`. + +```{tip} +To stage an installation without touching `/` — for packaging, or to inspect +what would be written — pass a prefix: `make systemd-install +DESTDIR=/tmp/simdb-staging`. +``` + ## Next steps - [Run with Docker Compose](run-with-docker.md) for an all-in-one deployment. +- [Deploy as a systemd service](#deploy-as-a-systemd-service) to keep it running + on a server. - [Set up PostgreSQL](set-up-postgresql.md) for production. - [Configure authentication](configure-authentication.md). - [Configure validation](configure-validation.md). diff --git a/docs/how-to/operate-server/run-behind-nginx-gunicorn.md b/docs/how-to/operate-server/run-behind-nginx-gunicorn.md index 8de94e2d..3755e60b 100644 --- a/docs/how-to/operate-server/run-behind-nginx-gunicorn.md +++ b/docs/how-to/operate-server/run-behind-nginx-gunicorn.md @@ -4,58 +4,39 @@ In production, run the SimDB server as a WSGI service behind a dedicated web server. This guide uses Gunicorn as the WSGI server and Nginx as the proxy/load-balancer. It assumes Nginx and Gunicorn are already installed. -## Set up the Gunicorn service +**For container deployments**, the sister project +[SimDB-Dashboard](https://github.com/iterorganization/SimDB-Dashboard) provides +a complete nginx configuration that proxies `/scenarios/api` to the SimDB +backend. This is the recommended approach for modern deployments. -Copy the init script from `src/simdb/remote/scripts/simdb.initd` in the SimDB -install directory to `/etc/init.d/simdb`. +## Set up Nginx (bare-metal) -Edit two lines in it: - -- `USER=simdb` to the user the workers should run as. -- `DAEMON=/home/simdb/venv/bin/gunicorn` to the `gunicorn` in your virtual - environment (find it with `which gunicorn` while the venv is active). - -Start and check the service: - -```bash -service simdb start -service simdb status -``` - -## Set up Nginx - -Create `/etc/nginx/conf.d/simdb.conf`: +Create `/etc/nginx/conf.d/simdb.conf`, for example: ```nginx server { listen 80; server_name localhost; # or the server's address - location / { - include proxy_params; + location /scenarios/api { + # Typical proxy params + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # Inform simdb backend to add this prefix in their responses + proxy_set_header X-Forwarded-Prefix /scenarios/api; + proxy_pass http://unix:/var/run/simdb.sock; + # For TCP proxy instead (e.g., if gunicorn binds to 0.0.0.0:5000): + # proxy_pass http://localhost:5000; } } ``` -The packaged `src/simdb/remote/scripts/simdb.nginx` can be copied instead. The -`proxy_pass` target must match the `BIND` value in the init script. - -If `/etc/nginx/proxy_params` does not exist, create it: - -```nginx -proxy_set_header Host $http_host; -proxy_set_header X-Real-IP $remote_addr; -proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; -proxy_set_header X-Forwarded-Proto $scheme; -``` - Make sure `/etc/nginx/nginx.conf` includes `/etc/nginx/conf.d/*.conf` inside its -`http {}` block, then reload: - -```bash -service nginx restart -``` +`http {}` block, then reload your nginx service. ## Allow large uploads diff --git a/docs/how-to/operate-server/run-dev-server.md b/docs/how-to/operate-server/run-dev-server.md index 0fa5ab62..c5bfef5d 100644 --- a/docs/how-to/operate-server/run-dev-server.md +++ b/docs/how-to/operate-server/run-dev-server.md @@ -38,5 +38,5 @@ Each API version publishes Swagger UI documentation, for example ## Troubleshooting the port If the server cannot bind to port 5000, another service is using it. Stop that -service, or change the port in the `simdb_server` script (find it with -`which simdb_server`). See also [Troubleshooting](../../troubleshooting.md). +service, or change the `server.port` setting in your SimDB configuration. See +also [Troubleshooting](../../troubleshooting.md). diff --git a/docs/how-to/operate-server/run-with-docker.md b/docs/how-to/operate-server/run-with-docker.md index 0ad372a5..db3d138e 100644 --- a/docs/how-to/operate-server/run-with-docker.md +++ b/docs/how-to/operate-server/run-with-docker.md @@ -76,6 +76,19 @@ match the published port. See the [server configuration reference](../../reference/server-configuration.md) for all options. +### Gunicorn runtime + +The `web` service runs Gunicorn with three `gthread` workers, four threads per +worker, and a 120-second request timeout by default. Override these settings +through the environment when starting Compose: + +```bash +GUNICORN_WORKERS=4 GUNICORN_THREADS=8 GUNICORN_TIMEOUT=180 docker compose up --build +``` + +The available variables are `GUNICORN_BIND`, `GUNICORN_WORKERS`, +`GUNICORN_WORKER_CLASS`, `GUNICORN_THREADS`, and `GUNICORN_TIMEOUT`. + ## Start ```bash @@ -113,14 +126,11 @@ docker compose --profile with_workers up --build They use the `[celery]` broker and result backend from `config/simdb.cfg`. -## Testing against multiple Python versions +## Building against a different Python version -The image accepts a `PYVER` build argument (the default is 3.12). -`docker-compose-pyver.yml` extends the base setup to build the server against -several Python versions at once, sharing one PostgreSQL and Redis instance. It -publishes a `web-311` service (Python 3.11, on port 5001) and a `web-313` -service (Python 3.13, on port 5003) alongside the default 3.12 service: +The image accepts a `PYVER` build argument, which selects the Python version +used as the build base; the default is 3.12: ```bash -docker compose -f docker-compose-pyver.yml up --build +docker build --build-arg PYVER=3.11 . ``` diff --git a/docs/how-to/use-the-dashboard.md b/docs/how-to/use-the-dashboard.md index 5b7c549b..d9c9da85 100644 --- a/docs/how-to/use-the-dashboard.md +++ b/docs/how-to/use-the-dashboard.md @@ -1,7 +1,8 @@ # Use the dashboard -Besides the CLI, a SimDB server has a web dashboard for browsing simulation -metadata in the browser. +Apart from using the SimDB CLI, a SimDB server can be explored via +[SimDB-Dashboard](https://github.com/iterorganization/SimDB-Dashboard) for browsing +simulation metadata in the browser. ## Open a simulation by UUID diff --git a/pyproject.toml b/pyproject.toml index 6de7bc86..3fcf96a3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,15 +55,15 @@ dependencies = [ [project.optional-dependencies] server = [ "Flask>=3.0", + "celery>=5.3.0", "flask-caching>=1.10", "flask-compress>=1.12", "flask-cors>=3", - "flask-mail~=0.9.1", "flask-restx>=1.0.0", + "gunicorn>=26.2.0", "python-magic~=0.4", - "simplejson~=3.0", - "celery>=5.3.0", "redis>=5.0.0", + "simplejson~=3.0", ] auth-ad = [ "easyad>=1.0", @@ -101,7 +101,7 @@ simdb_worker = "simdb.workers.cli:worker" simdb_beat = "simdb.workers.cli:beat" [project.urls] -Homepage = "https://simdb.iter.org/dashboard/" +Homepage = "https://simdb.iter.org/" Documentation = "https://simdb.readthedocs.io/en/latest/" Repository = "https://github.com/iterorganization/SimDB" diff --git a/requirements.txt b/requirements.txt index 26bef65d..cd91fa13 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,7 +15,6 @@ distro==1.8.0 setuptools>=59 pytest-cov~=3.0 email-validator~=1.1 -flask-mail~=0.9.1 semantic-version>=2.8 click>=7.0,<=8.1.8 PyJWT>=2.3 diff --git a/scripts/simdb b/scripts/simdb deleted file mode 100755 index 806ac02b..00000000 --- a/scripts/simdb +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env python3 -# PYTHON_ARGCOMPLETE_OK - -from simdb.cli import simdb - - -simdb.main() diff --git a/scripts/simdb-instance b/scripts/simdb-instance index d9dd3c41..ffe4d0ee 100755 --- a/scripts/simdb-instance +++ b/scripts/simdb-instance @@ -124,7 +124,7 @@ prepare_worktree() { for required_file in \ Dockerfile.dev \ docker-compose-dev.yml \ - deploy/nginx.dev.conf.template; do + docker/nginx.dev.conf.template; do [[ -f "$worktree/$required_file" ]] || die "$required_file is missing from commit $commit" done diff --git a/scripts/simdb_server b/scripts/simdb_server deleted file mode 100755 index 84d5816e..00000000 --- a/scripts/simdb_server +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env python3 - -from simdb.remote.wsgi import run - -run() diff --git a/src/simdb/remote/core/cache.py b/src/simdb/remote/core/cache.py index 64550951..ea3e15a5 100644 --- a/src/simdb/remote/core/cache.py +++ b/src/simdb/remote/core/cache.py @@ -5,7 +5,8 @@ from simdb.config import Config -config = Config("app.cfg") +# TODO remove hard-coded config_file path to "app.cfg" +config = Config(file_name="app.cfg") config.load() cache_options = { "CACHE_" + k.upper(): v for (k, v) in config.get_section("cache", {}).items() diff --git a/src/simdb/remote/scripts/simdb.initd b/src/simdb/remote/scripts/simdb.initd deleted file mode 100755 index a5fa574e..00000000 --- a/src/simdb/remote/scripts/simdb.initd +++ /dev/null @@ -1,62 +0,0 @@ -#! /bin/bash -### BEGIN INIT INFO -# Provides: simdb -# Required-Start: nginx -# Required-Stop: -# Default-Start: 2 3 4 5 -# Default-Stop: 0 1 6 -# Short-Description: SimDB Flask App -# Description: The gunicorn process that receives HTTP requests -# from nginx -# -### END INIT INFO -# -# Author: Jonathan Hollocombe -# -APPNAME=simdb -USER=simdb -PATH=/bin:/usr/bin:/sbin:/usr/sbin -APPMODULE=simdb.remote.wsgi:app -DAEMON=/home/simdb/venv/bin/gunicorn -BIND=unix:/var/run/simdb.sock -PIDFILE=/var/run/simdb.pid -LOGFILE=/var/log/simdb.log -WORKERS=3 - - -. /etc/rc.d/init.d/functions - - -if [ -e "/etc/default/$APPNAME" ] -then - . /etc/default/$APPNAME -fi - - -case "$1" in - start) - echo -n "Starting deferred execution scheduler" "$APPNAME" - $DAEMON --daemon --bind=$BIND --pid=$PIDFILE --workers=$WORKERS --user=$USER --log-file=$LOGFILE $APPMODULE - echo - exit $? - ;; - stop) - echo -n "Stopping deferred execution scheduler" "APPNAME" - killproc -p $PIDFILE gunicorn - echo - exit $? - ;; - force-reload|restart) - $0 stop - $0 start - ;; - status) - status -p $PIDFILE gunicorn && exit 0 || exit $? - ;; - *) - echo "Usage: /etc/init.d/$APPNAME {start|stop|restart|force-reload|status}" - exit 1 - ;; -esac - -exit 0 diff --git a/src/simdb/remote/scripts/simdb.nginx b/src/simdb/remote/scripts/simdb.nginx deleted file mode 100644 index 459b5a6d..00000000 --- a/src/simdb/remote/scripts/simdb.nginx +++ /dev/null @@ -1,9 +0,0 @@ -server { - listen 80; - server_name localhost; - - location / { - include proxy_params; - proxy_pass http://unix:/var/run/simdb.sock; - } -} diff --git a/uv.lock b/uv.lock index 4130318a..d3fa4577 100644 --- a/uv.lock +++ b/uv.lock @@ -3,10 +3,10 @@ revision = 3 requires-python = ">=3.11" resolution-markers = [ "python_full_version >= '3.13' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version == '3.12.*' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version < '3.12' and platform_machine == 'ARM64' and sys_platform == 'win32'", "(python_full_version >= '3.13' and platform_machine != 'ARM64') or (python_full_version >= '3.13' and sys_platform != 'win32')", + "python_full_version == '3.12.*' and platform_machine == 'ARM64' and sys_platform == 'win32'", "(python_full_version == '3.12.*' and platform_machine != 'ARM64') or (python_full_version == '3.12.*' and sys_platform != 'win32')", + "python_full_version < '3.12' and platform_machine == 'ARM64' and sys_platform == 'win32'", "(python_full_version < '3.12' and platform_machine != 'ARM64') or (python_full_version < '3.12' and sys_platform != 'win32')", ] @@ -892,16 +892,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/49/55/5bb1a2d918e9f02f131e47a59032bae70e48050e986e941511fd737a935c/flask_cors-6.0.5-py3-none-any.whl", hash = "sha256:68fcf75693e961f3af26683b23c4b9a8fb6b64de17d20d0c37b95e8de7ab2ed8", size = 16692, upload-time = "2026-06-08T20:20:16.247Z" }, ] -[[package]] -name = "flask-mail" -version = "0.9.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "blinker" }, - { name = "flask" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/05/2f/6a545452040c2556559779db87148d2a85e78a26f90326647b51dc5e81e9/Flask-Mail-0.9.1.tar.gz", hash = "sha256:22e5eb9a940bf407bcf30410ecc3708f3c56cc44b29c34e1726fe85006935f41", size = 45654, upload-time = "2014-09-28T23:35:22.329Z" } - [[package]] name = "flask-restx" version = "1.3.2" @@ -982,6 +972,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/93/e8/65e8707d00fe2a49bf12f609a9b2b39ba6dd23c2810eacad877c4fc94bfe/greenlet-3.5.4-cp315-cp315t-win_arm64.whl", hash = "sha256:08fc36de8442d5c3e95b044550dbea9bf144d31ec0cc58e36fb241cb6ef6a994", size = 250538, upload-time = "2026-07-22T11:40:17.985Z" }, ] +[[package]] +name = "gunicorn" +version = "26.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d9/8a/e4ef6ee11701b6cd64702848415ffb69eeff85cb388a3c6c7fe86f22f3f8/gunicorn-26.2.0.tar.gz", hash = "sha256:62b864895d9ebff0b2f9867ba04fe811c93121596540830c9c916d0769668447", size = 787921, upload-time = "2026-08-24T15:05:59.3Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/85/7522a52e5e2f42faf1a129113ab63e548c42e103e9af395b7bfe65e403e2/gunicorn-26.2.0-py3-none-any.whl", hash = "sha256:bd249d0b3f7972f7432f0a6b6ff3b3ee2d129f70cd1ff6c09a9dd9e29a2b88e3", size = 228389, upload-time = "2026-08-24T15:05:57.67Z" }, +] + [[package]] name = "h11" version = "0.16.0" @@ -1183,8 +1182,8 @@ all = [ { name = "flask-caching" }, { name = "flask-compress" }, { name = "flask-cors" }, - { name = "flask-mail" }, { name = "flask-restx" }, + { name = "gunicorn" }, { name = "imas-validator" }, { name = "psycopg2-binary" }, { name = "python-magic" }, @@ -1226,8 +1225,8 @@ server = [ { name = "flask-caching" }, { name = "flask-compress" }, { name = "flask-cors" }, - { name = "flask-mail" }, { name = "flask-restx" }, + { name = "gunicorn" }, { name = "python-magic" }, { name = "redis" }, { name = "simplejson" }, @@ -1259,8 +1258,8 @@ requires-dist = [ { name = "flask-caching", marker = "extra == 'server'", specifier = ">=1.10" }, { name = "flask-compress", marker = "extra == 'server'", specifier = ">=1.12" }, { name = "flask-cors", marker = "extra == 'server'", specifier = ">=3" }, - { name = "flask-mail", marker = "extra == 'server'", specifier = "~=0.9.1" }, { name = "flask-restx", marker = "extra == 'server'", specifier = ">=1.0.0" }, + { name = "gunicorn", marker = "extra == 'server'", specifier = ">=26.2.0" }, { name = "imas-python", specifier = ">=2.0.1" }, { name = "imas-simdb", extras = ["auth-ad", "auth-keycloak", "auth-ldap"], marker = "extra == 'auth'" }, { name = "imas-simdb", extras = ["imas-validator", "postgres", "server"], marker = "extra == 'all'" }, @@ -1679,8 +1678,8 @@ version = "2.5.1" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.13' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version == '3.12.*' and platform_machine == 'ARM64' and sys_platform == 'win32'", "(python_full_version >= '3.13' and platform_machine != 'ARM64') or (python_full_version >= '3.13' and sys_platform != 'win32')", + "python_full_version == '3.12.*' and platform_machine == 'ARM64' and sys_platform == 'win32'", "(python_full_version == '3.12.*' and platform_machine != 'ARM64') or (python_full_version == '3.12.*' and sys_platform != 'win32')", ] sdist = { url = "https://files.pythonhosted.org/packages/22/fd/89965aa4ac08c74998539fcbf24fa3540f3e15237fbeb6bcf9c908f4aade/numpy-2.5.1.tar.gz", hash = "sha256:a48a113e6afea91f5608793bafa7ef2ad481fefbda87ec5069f483de61cb9fa3", size = 20755553, upload-time = "2026-07-04T17:08:00.933Z" } @@ -2447,8 +2446,8 @@ version = "1.18.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.13' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version == '3.12.*' and platform_machine == 'ARM64' and sys_platform == 'win32'", "(python_full_version >= '3.13' and platform_machine != 'ARM64') or (python_full_version >= '3.13' and sys_platform != 'win32')", + "python_full_version == '3.12.*' and platform_machine == 'ARM64' and sys_platform == 'win32'", "(python_full_version == '3.12.*' and platform_machine != 'ARM64') or (python_full_version == '3.12.*' and sys_platform != 'win32')", ] dependencies = [ @@ -2611,8 +2610,8 @@ version = "9.1.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.13' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version == '3.12.*' and platform_machine == 'ARM64' and sys_platform == 'win32'", "(python_full_version >= '3.13' and platform_machine != 'ARM64') or (python_full_version >= '3.13' and sys_platform != 'win32')", + "python_full_version == '3.12.*' and platform_machine == 'ARM64' and sys_platform == 'win32'", "(python_full_version == '3.12.*' and platform_machine != 'ARM64') or (python_full_version == '3.12.*' and sys_platform != 'win32')", ] dependencies = [ @@ -2661,8 +2660,8 @@ version = "3.13.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.13' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version == '3.12.*' and platform_machine == 'ARM64' and sys_platform == 'win32'", "(python_full_version >= '3.13' and platform_machine != 'ARM64') or (python_full_version >= '3.13' and sys_platform != 'win32')", + "python_full_version == '3.12.*' and platform_machine == 'ARM64' and sys_platform == 'win32'", "(python_full_version == '3.12.*' and platform_machine != 'ARM64') or (python_full_version == '3.12.*' and sys_platform != 'win32')", ] dependencies = [