diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dc220db --- /dev/null +++ b/.gitignore @@ -0,0 +1,81 @@ +.idea + +### JetBrains template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + diff --git a/README.md b/README.md index 7a1380e..0c6ece7 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,167 @@ -# opsi +# opsi Docker Compose files -Docker Compose setup of a opsi software distribution and management system instance \ No newline at end of file +Docker Compose files to spin up an instance of [opsi](https://opsi.org) open source device management system. This specifically launches a so-called opsi Config Server. In opsi lingo Config Server is the central hub that includes among other components an opsi Depot Server. See also [opsi 4.3 English docs section "opsi Server"](https://docs.opsi.org/opsi-docs-en/4.3/server/overview.html) for reference. + +# How to run + +Add a `COMPOSE_ENV` file and save its location as a shell variable along with the location where this repo lives, here for example `/opt/containers/opsi` plus all other variables. At [env/fqdn_context.env.example](env/fqdn_context.env.example) you'll find an example environment file. + +When everything's ready start opsi with Docker Compose, otherwise head down to [Initial setup](#initial-setup) first. + +## Environment + +Make sure the upstream [github.com/opsi-org/opsi-docker](https://github.com/opsi-org/opsi-docker) repository is checked out locally. We're going with example dir `/opt/git/github.com/opsi-org/opsi-docker/branch/main`. We're also assuming that this repo exists at `/opt/containers/opsi`. + +``` +export UPSTREAM_REPO_DIR='/opt/git/github.com/opsi-org/opsi-docker/branch/main' +export UPSTREAM_COMPOSE_FILE="${UPSTREAM_REPO_DIR%/}"'/opsi-server/docker-compose.yml' +export UPSTREAM_ENV_FILE="${UPSTREAM_REPO_DIR%/}"'/opsi-server/opsi-server.env' +export CONTEXT='ux_vilnius' +export COMPOSE_PROJECT_NAME='opsi-'"${CONTEXT}" +export COMPOSE_ENV= +export COMPOSE_OVERRIDE='/opt/containers/opsi/compose.override.yaml' +``` + +In opsi's Git repo check out newest commit: + +``` +git -C "${UPSTREAM_REPO_DIR:?}" reset --hard origin/main +git -C "${UPSTREAM_REPO_DIR:?}" checkout main +git -C "${UPSTREAM_REPO_DIR:?}" pull +``` + +## Context + +On your deployment machine create the necessary Docker context to connect to and control the Docker daemon on whatever target host you'll be using, for example: +``` +docker context create fully.qualified.domain.name --docker 'host=ssh://root@fully.qualified.domain.name' +``` + +## Pull + +Pull images from Docker Hub verbatim. + +``` +docker compose \ + --project-name "${COMPOSE_PROJECT_NAME}" \ + --file "${UPSTREAM_COMPOSE_FILE}" \ + --file "${COMPOSE_OVERRIDE}" \ + --env-file "${UPSTREAM_ENV_FILE}" \ + --env-file "${COMPOSE_ENV}" \ + pull +``` + +## Copy to target + +Copy images to target Docker host, that is assuming you deploy to a machine that itself has no network route to reach Docker Hub or your private registry of choice. Copying in its simplest form involves a local `docker save` and a remote `docker load`. Consider the helper mini-project [quico.space/Quico/copy-docker](https://quico.space/Quico/copy-docker) where [copy-docker.sh](https://quico.space/Quico/copy-docker/src/branch/main/copy-docker.sh) allows the following workflow: + +``` +images="$(docker compose --project-name "${COMPOSE_PROJECT_NAME}" --file "${UPSTREAM_COMPOSE_FILE}" --file "${COMPOSE_OVERRIDE}" --env-file "${UPSTREAM_ENV_FILE}" --env-file "${COMPOSE_ENV}" config | grep -Pi -- 'image:' | awk '{print $2}' | sort | uniq)" +while IFS= read -u 10 -r image; do + copy-docker "${image}" fully.qualified.domain.name +done 10<<<"${images}" +``` + +This will for example copy over: + +``` +# docker image ls -a + +REPOSITORY TAG IMAGE ID CREATED SIZE +grafana/grafana latest 0a7de979b313 2 weeks ago 723MB +redis/redis-stack-server latest 1ebedd176a23 7 weeks ago 513MB +uibmz/opsi-server 4.3 f07683f1828b 2 months ago 996MB +mariadb 10.7 895b6c8829c3 2 years ago 396MB +``` + +## Start + +``` +docker \ + --context 'fully.qualified.domain.name' \ + compose \ + --project-name "${COMPOSE_PROJECT_NAME}" \ + --file "${UPSTREAM_COMPOSE_FILE}" \ + --file "${COMPOSE_OVERRIDE}" \ + --env-file "${UPSTREAM_ENV_FILE}" \ + --env-file "${COMPOSE_ENV}" \ + up --detach +``` + +## Clean up + +``` +docker --context 'fully.qualified.domain.name' system prune -af +docker system prune -af +``` + +# Initial setup + +We're assuming you run Docker Compose workloads with ZFS-based bind mounts. ZFS management, creating a zpool and setting adequate properties for its datasets is out of scope of this document. + +## Datasets + +Create ZFS datasets and set permissions as needed. + +* Parent dateset + ``` + export "$(grep -Pi -- '^CONTEXT=' "${COMPOSE_ENV}" | xargs --max-lines 1)" + zfs create -o canmount=off zpool/data/opt + zfs create -o mountpoint=/opt/docker-data zpool/data/opt/docker-data + ``` + +* Container-specific datasets + ``` + zfs create -p 'zpool/data/opt/docker-data/opsi-'"${CONTEXT}"'/grafana/data' + zfs create -p 'zpool/data/opt/docker-data/opsi-'"${CONTEXT}"'/mysql/data' + zfs create -p 'zpool/data/opt/docker-data/opsi-'"${CONTEXT}"'/opsi/data' + zfs create -p 'zpool/data/opt/docker-data/opsi-'"${CONTEXT}"'/redis/data' + ``` + All four opsi components, that it Grafana, MySQL, opsi and Redis have one subdirectory for their `data` volume. None of them require a `config` volume or other mounts. + +* Create subdirs + ``` + mkdir -p '/opt/docker-data/opsi-'"${CONTEXT}"'/opsi/'{'.ssh','config','data','projects'} + ``` + +* Change ownership + ``` + chown -R 472:472 'zpool/data/opt/docker-data/opsi-'"${CONTEXT}"'/grafana/data' + ``` + Here you'll want to accommodate the Grafana container. Per its [upstream Dockerfile](https://github.com/grafana/grafana/blob/main/Dockerfile) retrieved from `Dockerfile` in the [github.com/grafana/grafana](https://github.com/grafana/grafana) repository in August 2025 [all Grafana images that end up at Docker Hub](https://hub.docker.com/r/grafana/grafana/tags) are built with the directive: + ``` + USER "$GF_UID" + ``` + Where `"$GF_UID"` is populated with `ARG GF_UID="472"`. + +## Additional files + +With preparations out of the way opsi does not require any additional files present on your Docker host's filesystem, as bind mounts or via other means. + +Head back up to [How to run](#how-to-run). + +# Development + +## Conventional commits + +This project uses [Conventional Commits](https://www.conventionalcommits.org/) for its commit messages. + +### Commit types + +Commit _types_ besides `fix` and `feat` are: + +- `refactor`: Keeping functionality while streamlining or otherwise improving function flow +- `docs`: Documentation for project or components + +### Commit scopes + +The following _scopes_ are known for this project. A Conventional Commits commit message may optionally use one of the following scopes or none: + +- `mysql`: A change to how the `mysql` service component works +- `redis`: A change to how the `redis` service component works +- `grafana`: A change to how the `grafana` service component works +- `opsi_server`: A change to how the `opsi_server` service component works +- `build`: Build-related changes such as `Dockerfile` fixes and features. +- `mount`: Volume or bind mount-related changes. +- `net`: Networking, IP addressing, routing changes +- `meta`: Affects the project's repo layout, file names etc. diff --git a/compose.override.yaml b/compose.override.yaml new file mode 100644 index 0000000..2e5c91e --- /dev/null +++ b/compose.override.yaml @@ -0,0 +1,68 @@ +x-container-defaults: &container-defaults + environment: + TZ: "${TIMEZONE:-Etc/UTC}" + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "10" + compress: "true" + networks: !override + opsi-default: + restart: "${RESTARTPOLICY:-always}" +x-common-grafana-variables: &common-grafana-variables + GF_SECURITY_ADMIN_USER: "${GF_SECURITY_ADMIN_USER}" + GF_SECURITY_ADMIN_PASSWORD: "${GF_SECURITY_ADMIN_PASSWORD}" +x-common-mysql-variables: &common-mysql-variables + MYSQL_DATABASE: "${MYSQL_DATABASE}" + MYSQL_USER: "${MYSQL_USER}" + MYSQL_PASSWORD: "${MYSQL_PASSWORD}" +x-common-redis-variables: &common-redis-variables + REDIS_PASSWORD: "${REDIS_PASSWORD}" +services: + grafana: + <<: [ *container-defaults ] + container_name: "opsi-grafana-${CONTEXT}" + environment: + <<: [ *common-grafana-variables ] + volumes: + - "/opt/docker-data/opsi-${CONTEXT}/grafana/data:/var/lib/grafana" + mysql: + <<: [ *container-defaults ] + container_name: "opsi-mysql-${CONTEXT}" + environment: + <<: [ *common-mysql-variables ] + MYSQL_ROOT_PASSWORD: "${MYSQL_ROOT_PASSWORD}" + volumes: + - "/opt/docker-data/opsi-${CONTEXT}/mysql/data:/var/lib/mysql" + opsi-server: + <<: [ *container-defaults ] + container_name: "opsi-opsi_configserver-${CONTEXT}" + domainname: "${DOMAINNAME}" + environment: + <<: [ *common-grafana-variables, *common-mysql-variables, *common-redis-variables ] + OPSI_ADMIN_PASSWORD: "${OPSI_ADMIN_PASSWORD}" + OPSI_ROOT_PASSWORD: "${OPSI_ROOT_PASSWORD}" + OPSICONFD_LOG_LEVEL: "${OPSICONFD_LOG_LEVEL:-6}" + OPSICONFD_LOG_LEVEL_FILE: "${OPSICONFD_LOG_LEVEL_FILE:-4}" + OPSICONFD_TRUSTED_PROXIES: "${OPSICONFD_TRUSTED_PROXIES}" + hostname: "${HOSTNAME}" + volumes: + - "/opt/docker-data/opsi-${CONTEXT}/opsi/data:/data" + redis: + <<: [ *container-defaults ] + container_name: "opsi-redis-${CONTEXT}" + environment: + <<: [ *common-redis-variables ] + volumes: + - "/opt/docker-data/opsi-${CONTEXT}/redis/data:/data" +networks: !override + opsi-default: + name: "opsi-${CONTEXT}" + driver: "bridge" + driver_opts: + com.docker.network.enable_ipv6: "false" + ipam: + driver: "default" + config: + - subnet: "${SUBNET}" diff --git a/env/fqdn_context.env.example b/env/fqdn_context.env.example new file mode 100644 index 0000000..d7eb0f8 --- /dev/null +++ b/env/fqdn_context.env.example @@ -0,0 +1,20 @@ +CONTEXT=ux_vilnius +DOMAINNAME=example.com +GF_SECURITY_ADMIN_PASSWORD=top-secret-s578 +GF_SECURITY_ADMIN_USER=admin +HOSTNAME=opsi +MYSQL_DATABASE=opsi +MYSQL_PASSWORD=top-secret-qM5i +MYSQL_ROOT_PASSWORD=top-secret-xpu5 +MYSQL_USER=opsi +OPSICONFD_LOG_LEVEL=6 +OPSICONFD_LOG_LEVEL_FILE=4 +OPSICONFD_TRUSTED_PROXIES=[172.16.0.0/12, 127.0.0.1/32, ::1/128] +OPSI_ADMIN_PASSWORD=top-secret-HjK5 +OPSI_ROOT_PASSWORD=top-secret-W9Au +REDIS_PASSWORD=top-secret-M2tu +SUBNET=172.30.95.0/24 +TIMEZONE=America/Aruba + +# Other available defaults +# RESTARTPOLICY=always