Private LAN platform for one Debian/Ubuntu box: HTTPS portal, message queue, cache, databases, Docker registry, CI runner, Maven and Debian repos. Ansible + Docker Compose, no cloud.
Host: devops.local. Portal: https://devops.local/ (English by default, EN/RU toggle). Trust the CA once: http://devops.local/ca.crt.
| RabbitMQ | PostgreSQL · Adminer |
|---|---|
![]() | ![]() |
| MongoDB · express | Maven · Reposilite |
|---|---|
![]() | ![]() |
Web UIs sit behind nginx HTTPS. AMQP / MQTT / Redis / Postgres / Mongo / Elasticsearch stay on native ports.
| URL | |
|---|---|
| Portal | https://devops.local/ |
| CA | http://devops.local/ca.crt |
| Portainer | https://devops.local/portainer/ (admin) |
| Registry UI | https://devops.local/registry/ |
| Registry API | https://devops.local/v2/ · docker login devops.local |
| RabbitMQ | https://devops.local/rabbitmq/ · AMQP :5672 · MQTT :1883 · WS :15675 |
| Redis Commander | https://devops.local/redis/ · Redis :6379 |
| PostgreSQL | https://devops.local/postgres/ · :5432 |
| MongoDB | https://devops.local/mongo/ · :27017 |
| Elasticsearch | https://devops.local/elastic/ · :9200 |
| Maven | https://devops.local/maven/ |
| Debian APT | https://devops.local/debian/ |
| Gitea MCP | http://devops.local:8080/mcp |
| Local Gitea | http://devops.local:3000/ (optional, GITEA_ENABLED=true) |
Docs: Maven / APT · databases · TLS / docker login · Gitea.
docker login devops.local
docker tag myimg devops.local/myimg
docker push devops.local/myimg| Stack | On the host | What it runs |
|---|---|---|
devops | /opt/compose/devops | runner, MCP, Maven, Debian, registry, Portainer, TLS/CA; local Gitea if GITEA_ENABLED=true |
aiml | /opt/compose/aiml | Open WebUI + Speaches (STT/TTS, CPU) · optional Ollama |
monitoring | /opt/compose/monitoring | Grafana + Prometheus + Loki / Alloy (container logs) |
middleware | /opt/compose/middleware | nginx portal :80→:443, RabbitMQ (+MQTT), Redis, Postgres, Mongo, ES |
Each service is optional (make setup toggles / .env*_ENABLED). Compose profiles follow those flags.
Runner / MCP always use gitea_instance_url (existing Synology Gitea) — they are not rewritten to :3000. See docs/gitea.md.
Data lives on the host (bind mounts, not named volumes):
| Path | Contents |
|---|---|
/opt/compose/tls | Private CA + nginx server cert (owned by devops) |
/opt/compose/middleware/data/rabbitmq | RabbitMQ |
/opt/compose/middleware/data/redis | Redis AOF/RDB |
/opt/compose/middleware/data/postgres | PostgreSQL |
/opt/compose/middleware/data/mongo | MongoDB |
/opt/compose/middleware/data/elasticsearch | Elasticsearch |
/opt/compose/devops/data/runner | act_runner state |
/opt/compose/devops/data/maven | Reposilite |
/opt/compose/devops/data/debian | reprepro pool / dists / incoming |
/opt/compose/devops/data/gitea | Local Gitea (SQLite + repos) |
/opt/compose/devops/data/debian-gpg | APT signing key |
/opt/compose/devops/data/registry | Registry blobs |
/opt/compose/devops/data/portainer | Portainer |
/opt/compose/aiml/data/ollama | Ollama models (optional) |
/opt/compose/aiml/data/speaches | Speaches STT/TTS models |
/opt/compose/aiml/data/openwebui | Open WebUI |
/opt/compose/monitoring/data/prometheus | Prometheus TSDB |
/opt/compose/monitoring/data/loki | Loki chunks / index |
/opt/compose/monitoring/data/alloy | Alloy state |
/opt/compose/monitoring/data/grafana | Grafana |
First deploy: Ansible creates host bind-mount dirs under /opt/compose/{devops,aiml,monitoring,middleware}/data.
Ansible runs in WSL. On Windows, ansible.ps1 wraps wsl make. Native Windows is not a supported control node.
Prerequisites: WSL with make + python3-venv, SSH key to darvik@devops.local, sudo on the target.
# WSL
sudo apt update && sudo apt install -y make python3-venv python3-pip
cd /mnt/d/Projects/devops/dockerDo not edit tracked inventory for secrets. make setup writes a gitignored .env (component toggles, credentials, optional CA paths); deploy targets read it via .env.yml + inventory/local.ini.
make setup # components + secrets + optional bring-your-own CA
make ping
make docker # Docker CE + Compose plugin
make devops # TLS + registry/portainer/runner/… (before middleware)
make aiml # Open WebUI + Speaches (+ Ollama if enabled)
make monitoring # Grafana + Prometheus + Loki/Alloy
make middleware # portal + data services
make deploy # all of the above (site.yml)Windows:
.\ansible.ps1 setup
.\ansible.ps1 ping
.\ansible.ps1 docker
.\ansible.ps1 devops
.\ansible.ps1 aiml
.\ansible.ps1 monitoring
.\ansible.ps1 middleware
.\ansible.ps1 deployOther targets: make install, make check, make version, make clean, make help.
The Makefile uses project .venv and sets ANSIBLE_CONFIG / ANSIBLE_ROLES_PATH / ANSIBLE_INVENTORY (needed on world-writable /mnt/d/...).
Playbook issues a private CA + server cert, installs the CA into OS trust and /etc/docker/certs.d/ on devops.local, and does not use insecure-registries. LAN clients need the same CA (docs/registry-tls.md).
Portainer: user admin, password from .env (PORTAINER_PASSWORD, min 12 chars). --admin-password-file only seeds the first init. If login fails after a previous deploy, set PORTAINER_FORCE_REINIT=true once, make devops, then set it back to false (wipes Portainer data).
Grafana: https://devops.local/grafana/ — admin user/password from .env (GRAFANA_ADMIN_*). Logging (local Docker + remote hosts / bare-metal via Alloy agents): docs/logging.md.
media-vn (Synology) → devops.local step-by-step (RU):docs/logging-media-vn.md
(agent = Docker container + shared volume / syslog; no host docker.sock).
make devops issues TLS under /opt/compose/tls and brings up registry/portainer/runner/….
New portal card: add portal_services in inventory/group_vars/middleware.yml and, if needed, location = /status/... in stacks/middleware/nginx/default.conf.j2. Bilingual copy: portal_i18n + title / description{ en, ru }.
Refresh screenshots (needs .env and a reachable portal; portal is forced to English):
npm install
npx playwright install chromium
PORTAL_URL=https://devops.local npm run screenshotsCursor MCP:
{
"mcpServers": {
"gitea": {
"url": "http://devops.local:8080/mcp",
"headers": {
"Authorization": "Bearer <gitea_mcp_access_token>"
}
}
}
}Credentials and host/URL overrides: .env (make setup). Defaults live in inventory/group_vars/ — do not put secrets there.
| Variable | Default | Description |
|---|---|---|
gitea_enabled | false | Local Gitea. .env: GITEA_ENABLED |
runner_enabled / mcp_enabled / maven_enabled / debian_enabled | true | DevOps toggles |
ollama_enabled | false | Local LLM API (OLLAMA_ENABLED=true to enable) |
speaches_enabled / openwebui_enabled | true | Voice STT/TTS + chat UI |
prometheus_enabled / grafana_enabled / loki_enabled | true | Metrics + Grafana UI + Loki/Alloy logs |
ollama_models | gemma2:2b,qwen3:1.7b | Pulled when Ollama enabled |
speaches_tts_default | kokoro | Default TTS in Open WebUI (kokoro or piper; both preloaded) |
registry_enabled / portainer_enabled | true | Registry + Portainer (devops stack) |
rabbitmq_enabled / redis_enabled / postgres_enabled / mongo_enabled / elastic_enabled | true | Middleware toggles |
portal_lang | en | Portal default language (en / ru); UI toggle still works |
tls_ca_source_crt / tls_ca_source_key | `` | Optional local CA paths (from make setup) |
gitea_instance_url | Synology URL | Runner/MCP target. Not rewritten to local :3000 |
gitea_http_port / gitea_ssh_port | 3000 / 2222 | Local Gitea HTTP / SSH |
gitea_root_url | http://devops.local:3000/ | Local server ROOT_URL only |
gitea_runner_name | devops.local | Runner name in Gitea |
gitea_runner_labels | devops.local:docker://catthehacker/ubuntu:act-latest | Runner labels |
gitea_mcp_port | 8080 | Host port for gitea-mcp HTTP |
maven_http_port | 8083 | Reposilite on localhost (nginx /maven/) |
debian_http_port | 8084 | Debian www on localhost (nginx /debian/) |
gitea_runner_force_reregister | false | Delete .runner and register again |
middleware_http_port | 80 | Nginx HTTP → HTTPS redirect + /ca.crt |
middleware_https_port | 443 | Nginx portal, UIs, registry /v2/ |
tls_enabled | true | Private CA + server cert for nginx |
tls_extra_dns / tls_extra_ips | [] | Extra SANs on the server cert |
rabbitmq_amqp_port | 5672 | RabbitMQ AMQP |
rabbitmq_mqtt_port | 1883 | RabbitMQ MQTT (same plugins as iot-platform-go) |
rabbitmq_mqtt_ws_port | 15675 | RabbitMQ MQTT over WebSocket |
redis_port | 6379 | Redis |
postgres_port | 5432 | PostgreSQL |
mongo_port | 27017 | MongoDB |
elastic_http_port | 9200 | Elasticsearch HTTP |
elastic_java_opts | -Xms512m -Xmx512m | ES heap |
compose_root | /opt/compose | Remote directory for stacks |
Do not commit .env, .env.yml, or inventory/local.ini.
- Add
stacks/<name>/docker-compose.ymland.env.j2 - Add an inventory group and playbook that applies
compose_stackwithcompose_stack_name: <name>
Old passlib + bcrypt 5.x is incompatible. This repo hashes via scripts/bcrypt_hash.py. Reinstall venv deps:
make install
make middlewareBroken system Ansible. Use the Makefile venv only:
make clean
make install
make deployThe runner keeps a hidden /opt/compose/devops/data/runner/.runner after first register. If the token was reset or the runner was removed in Gitea UI, Declare returns 500. Registration env vars are ignored while .runner exists.
ssh darvik@devops.local
sudo docker compose -f /opt/compose/devops/docker-compose.yml stop devops-runner
sudo rm -f /opt/compose/devops/data/runner/.runner
sudo docker compose -f /opt/compose/devops/docker-compose.yml up -d devops-runnerOr set GITEA_RUNNER_FORCE_REREGISTER=true once and make devops, then set it back to false. Use a fresh token from Gitea → Site Administration → Actions → Runners.
ssh darvik@devops.local
sudo docker compose -f /opt/compose/devops/docker-compose.yml ps
sudo docker compose -f /opt/compose/devops/docker-compose.yml logs
sudo docker compose -f /opt/compose/middleware/docker-compose.yml ps
sudo docker compose -f /opt/compose/middleware/docker-compose.yml logs




