From 0f12c3fa7470bae0eeb21dcdf76cf56523f46963 Mon Sep 17 00:00:00 2001 From: Johannes Schriewer Date: Sat, 11 Jan 2025 04:10:20 +0100 Subject: [PATCH] Add Docker files (and Docker compose stack) and document usage --- Dockerfile | 21 ++++++++ Readme.md | 95 ++++++++++++++++++++++++++++++++--- default.env | 17 +++++++ docker-compose.yaml | 46 +++++++++++++++++ entrypoint.sh | 8 +++ inventory_project/settings.py | 39 ++++++++------ 6 files changed, 204 insertions(+), 22 deletions(-) create mode 100644 Dockerfile create mode 100644 default.env create mode 100644 docker-compose.yaml create mode 100644 entrypoint.sh diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..25ee906 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,21 @@ +FROM python:3.13-alpine + +WORKDIR /usr/src/app + +RUN pip install --no-cache-dir poetry + +ENV INVENTORY_STATIC_FILES=/static INVENTORY_MEDIA_FILES=/media +RUN mkdir -p "$INVENTORY_MEDIA_FILES" && mkdir -p "$INVENTORY_STATIC_FILES" + +COPY pyproject.toml /usr/src/app +COPY poetry.lock /usr/src/app + +RUN poetry install --no-root --no-interaction --no-cache +COPY manage.py ./manage.py +COPY inventory ./inventory +COPY inventory_project ./inventory_project +COPY entrypoint.sh /entrypoint.sh + +RUN poetry run python manage.py collectstatic + +CMD [ "/bin/sh", "/entrypoint.sh" ] \ No newline at end of file diff --git a/Readme.md b/Readme.md index 9add83e..7c6ca70 100644 --- a/Readme.md +++ b/Readme.md @@ -9,17 +9,19 @@ what area of your workshop you have to search for to find the part you currently need. It has been optimized to store information for electronics parts and small other hardware like screws, nuts and bolts. -### Prerequisites +### Prerequisites for manual install or docker Standalone As configured by default you will need the following: - A postgres database named `inventory` with a postgres user `inventory` that may connect without password or by default with the password `inventory` + +### Installation (manual) + +You will need: - Python > 3.10 - Poetry to install requirements and create a virtualenv -### Installation - This is a standard Django 5.1 application, if you know how to deploy those the following might sound familiar: @@ -27,16 +29,28 @@ following might sound familiar: - Github `git clone https://github.com/dunkelstern/inventory.git` - ForgeJo: `git clone https://git.dunkelstern.de/dunkelstern/inventory.git` 2. Change to checkout: `cd inventory` -3. Install virtualenv and dependencies: `poetry install --no-root` +3. Install virtualenv and dependencies: + ``` + poetry install --no-root + ``` 4. If you want to use the system in another language than the default english set it up in the `inventory_project/settings.py`: ```python LANGUAGE_CODE = 'en-us' # or something like 'de-de' ``` see the settings file for defined languages. -5. If you changed the language rebuild the translation files: `poetry run python manage.py compilemessages` -6. Migrate the Database: `poetry run python manage.py migrate` -7. Create an admin user: `poetry run python manage.py createsuperuser` +5. If you changed the language rebuild the translation files: + ``` + poetry run python manage.py compilemessages + ``` +6. Migrate the Database: + ``` + poetry run python manage.py migrate + ``` +7. Optionally create an admin user. If not done manually the application will prompt you on first run. + ``` + poetry run python manage.py createsuperuser + ``` 8. Run the server - Development server (not for deployment!): `poetry run python manage.py runserver` - Deployment via `gunicorn` on port 8000: `poetry run gunicorn inventory_project.wsgi -b 0.0.0.0:8000` @@ -44,6 +58,73 @@ following might sound familiar: Then login on `http://localhost:8000/admin/` for the Django admin interface or go to `http://localhost:8000` to enter the inventory management system directly +### Installation (Standalone Docker) + +#### Building yourself + +1. Checkout repository: + - Github `git clone https://github.com/dunkelstern/inventory.git` + - ForgeJo: `git clone https://git.dunkelstern.de/dunkelstern/inventory.git` +2. Change to checkout: `cd inventory` +3. Build Docker image: `docker build -t 'dunkelstern/inventory:latest' .` + +next steps below + +#### Pulling from docker hub + +1. Pull Docker image: `docker pull 'dunkelstern/inventory:latest'` + +next steps below + +#### Next steps + +1. Install a PostgreSQL DB somewhere and create a user and DB. +2. Setup environment (put everything in a `.env` file): + ``` + INVENTORY_DB_HOST= + INVENTORY_DB_NAME= + INVENTORY_DB_USER= + INVENTORY_DB_PASSWORD= + + INVENTORY_SECRET_KEY= + INVENTORY_EXTERNAL_URL=http://localhost:8000 + INVENTORY_DEBUG=FALSE + + INVENTORY_LANGUAGE=en-us + INVENTORY_TIMEZONE=UTC + + INVENTORY_PAGE_SIZE=25 + ``` +3. Create a media directory for uploaded files: `mkdir -p media` +4. Run the container: + ``` + docker run \ + --name inventory \ + -d \ + --restart=always \ + --env-file=.env \ + -p 8000:8000 \ + --volume ./media:/media \ + dunkelstern/inventory:latest + ``` +5. The onboarding process will start on first call of the application and prompt to create an admin user. + +### Installation (Docker compose) + +1. Checkout repository: + - Github `git clone https://github.com/dunkelstern/inventory.git` + - ForgeJo: `git clone https://git.dunkelstern.de/dunkelstern/inventory.git` +2. Change to checkout: `cd inventory` +3. Copy `default.env` to `override.env` and check settings. Use a long random string for `INVENTORY_SECRET_KEY`! +4. Build the stack: `docker-compose up --build -d` +5. You can reach the application on port 8000 +6. The onboarding process will start on first call of the application and prompt to create an admin user. + +The compose stack will create two volumes: + +- `inventory_dbdata` which contains the PostgreSQL database directory +- `inventory_mediafiles` which will contain any uploaded file + ### Additional information 1. The initial DB migration pre-populates the database with some useful defaults diff --git a/default.env b/default.env new file mode 100644 index 0000000..f3ee62a --- /dev/null +++ b/default.env @@ -0,0 +1,17 @@ +# override this with a long random string (used for CSRF protection) +INVENTORY_SECRET_KEY="" + +# override with URL the service will be available under +INVENTORY_EXTERNAL_URL="https://inventory.example.com" + +# keep this to FALSE for deployments +INVENTORY_DEBUG="FALSE" + +# if you want to run the service in another language, override this +INVENTORY_LANGUAGE="en-us" + +# if you want the service to use local time then override this +INVENTORY_TIMEZONE="UTC" + +# This is the default pagination size +INVENTORY_PAGE_SIZE="25" \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..feff607 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,46 @@ +name: inventory + +services: + db: + image: postgres:17-alpine + restart: always + shm_size: 128mb + environment: + POSTGRES_PASSWORD: inventory + POSTGRES_USER: inventory + POSTGRES_DB: inventory + volumes: + - dbdata:/var/lib/postgresql/data + + inventory: + image: dunkelstern/inventory + build: . + restart: always + depends_on: + - db + env_file: + - path: ./default.env + required: true + - path: ./override.env + required: false + environment: + INVENTORY_DB_HOST: db + INVENTORY_DB_NAME: inventory + INVENTORY_DB_USER: inventory + INVENTORY_DB_PASSWORD: inventory + ports: + - name: web + target: 8000 + host_ip: 127.0.0.1 + published: "8000" + protocol: tcp + app_protocol: http + mode: host + volumes: + - mediafiles:/media + links: + - db + +volumes: + dbdata: + mediafiles: diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 0000000..96c507e --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,8 @@ +#!/bin/sh + +cd /usr/src/app + +# poetry run python manage.py collectstatic --noinput +poetry run python manage.py migrate --noinput + +exec poetry run gunicorn inventory_project.wsgi -b 0.0.0.0:8000 \ No newline at end of file diff --git a/inventory_project/settings.py b/inventory_project/settings.py index f76b75e..8813830 100644 --- a/inventory_project/settings.py +++ b/inventory_project/settings.py @@ -10,10 +10,12 @@ For the full list of settings and their values, see https://docs.djangoproject.com/en/3.0/ref/settings/ """ -from typing import List import os import sys import asyncio +import socket +from urllib.parse import urlparse +from uuid import uuid4 from django.utils.translation import gettext_lazy as _ if sys.platform == 'win32': @@ -23,19 +25,25 @@ if sys.platform == 'win32': BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # Externally visible URL of the server -SERVER_URL = "http://127.0.0.1:8000" +SERVER_URL = os.environ.get('INVENTORY_EXTERNAL_URL', "http://127.0.0.1:8000") # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = 'nqo*a(^g$8#0%&+*_7#b_7ybn-znk4#=45_(qy-lq-^v675pqk' +SECRET_KEY = os.environ.get('INVENTORY_SECRET_KEY', uuid4().hex) # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = True - -ALLOWED_HOSTS: List[str] = [] +DEBUG = (os.environ.get('INVENTORY_DEBUG', "FALSE") == "TRUE") +parsed_url = urlparse(SERVER_URL) +ALLOWED_HOSTS: list[str] = [ + '.localhost', + '127.0.0.1', + '[::1]', + parsed_url.hostname, + socket.gethostbyname('localhost') +] # Application definition @@ -89,9 +97,10 @@ ASGI_APPLICATION = 'inventory_project.asgi.application' DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql', - 'NAME': 'inventory', - 'USER': 'inventory', - 'PASSWORD': 'inventory' + 'HOST': os.environ.get('INVENTORY_DB_HOST', 'localhost'), + 'NAME': os.environ.get('INVENTORY_DB_NAME', 'inventory'), + 'USER': os.environ.get('INVENTORY_DB_USER', 'inventory'), + 'PASSWORD': os.environ.get('INVENTORY_DB_PASSWORD', 'inventory') } } @@ -122,9 +131,9 @@ LANGUAGES = [ ("en", _("English")), ] -LANGUAGE_CODE = 'en-us' +LANGUAGE_CODE = os.environ.get('INVENTORY_LANGUAGE', 'en-us') -TIME_ZONE = 'UTC' +TIME_ZONE = os.environ.get('INVENTORY_TIMEZONE', 'UTC') USE_I18N = True @@ -137,13 +146,13 @@ USE_TZ = True # https://docs.djangoproject.com/en/3.0/howto/static-files/ STATIC_URL = '/static/' -STATIC_ROOT = os.path.join(BASE_DIR, 'static') +STATIC_ROOT = os.environ.get('INVENTORY_STATIC_FILES', os.path.join(BASE_DIR, 'static')) STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage' MEDIA_URL = '/media/' -MEDIA_ROOT = os.path.join(BASE_DIR, 'media') -SERVE_MEDIA_FILES = DEBUG +MEDIA_ROOT = os.environ.get('INVENTORY_MEDIA_FILES', os.path.join(BASE_DIR, 'media')) +SERVE_MEDIA_FILES = True # Default page size for paginated content -PAGE_SIZE = 25 +PAGE_SIZE = int(os.environ.get('INVENTORY_PAGE_SIZE', "25"))