feat(build): Initial commit

This commit is contained in:
2025-08-27 17:36:50 +02:00
parent e4359aa5e2
commit 1568f8ce29
4 changed files with 335 additions and 2 deletions

81
.gitignore vendored Normal file
View File

@@ -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

168
README.md
View File

@@ -1,3 +1,167 @@
# opsi # opsi Docker Compose files
Docker Compose setup of a opsi software distribution and management system instance 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=<add accordingly>
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.

68
compose.override.yaml Normal file
View File

@@ -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}"

20
env/fqdn_context.env.example vendored Normal file
View File

@@ -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