Implemented all service roles
- docker: install Docker Engine from official apt repo, configure log rotation, create shared traefik_public network - traefik: reverse proxy with automatic Let's Encrypt TLS, dashboard on cloud.ladkau.de behind basic auth - gitea: self-hosted Git with PostgreSQL, Git-over-SSH on port 2222 - sso: Keycloak with PostgreSQL for OIDC/OAuth2 single sign-on - nextcloud: file storage with PostgreSQL, Redis, cron sidecar, CalDAV/CardDAV well-known redirects - mail: Roundcube webmail client with PostgreSQL - registry: Docker Registry v2 with htpasswd auth, no upload size limit - k8s: placeholder nginx page All secrets documented in vault.yml; runbook updated with per-service first-run notes.
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
- name: Restart docker
|
||||
ansible.builtin.service:
|
||||
name: docker
|
||||
state: restarted
|
||||
@@ -1,2 +1,93 @@
|
||||
---
|
||||
# TODO: implement docker role
|
||||
# Install Docker Engine from Docker's official apt repository.
|
||||
# The Ubuntu-provided docker.io package is not used — it lags behind upstream.
|
||||
|
||||
# --- Prerequisites ---
|
||||
|
||||
- name: Install apt transport packages
|
||||
ansible.builtin.apt:
|
||||
name:
|
||||
- ca-certificates
|
||||
- gnupg
|
||||
state: present
|
||||
update_cache: true
|
||||
tags: docker
|
||||
|
||||
- name: Create /etc/apt/keyrings directory
|
||||
ansible.builtin.file:
|
||||
path: /etc/apt/keyrings
|
||||
state: directory
|
||||
mode: "0755"
|
||||
tags: docker
|
||||
|
||||
- name: Download Docker GPG key
|
||||
ansible.builtin.get_url:
|
||||
url: https://download.docker.com/linux/ubuntu/gpg
|
||||
dest: /etc/apt/keyrings/docker.asc
|
||||
mode: "0644"
|
||||
force: false
|
||||
tags: docker
|
||||
|
||||
- name: Add Docker apt repository
|
||||
ansible.builtin.apt_repository:
|
||||
repo: >-
|
||||
deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.asc]
|
||||
https://download.docker.com/linux/ubuntu
|
||||
{{ ansible_distribution_release }} stable
|
||||
filename: docker
|
||||
state: present
|
||||
tags: docker
|
||||
|
||||
# --- Install ---
|
||||
|
||||
- name: Install Docker Engine and Compose plugin
|
||||
ansible.builtin.apt:
|
||||
name:
|
||||
- docker-ce
|
||||
- docker-ce-cli
|
||||
- containerd.io
|
||||
- docker-buildx-plugin
|
||||
- docker-compose-plugin
|
||||
state: present
|
||||
update_cache: true
|
||||
notify: Restart docker
|
||||
tags: docker
|
||||
|
||||
# --- Daemon configuration ---
|
||||
|
||||
- name: Deploy Docker daemon config
|
||||
ansible.builtin.template:
|
||||
src: daemon.json.j2
|
||||
dest: /etc/docker/daemon.json
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
notify: Restart docker
|
||||
tags: docker
|
||||
|
||||
# --- Service ---
|
||||
|
||||
- name: Enable and start Docker
|
||||
ansible.builtin.service:
|
||||
name: docker
|
||||
state: started
|
||||
enabled: true
|
||||
tags: docker
|
||||
|
||||
# --- Deploy user access ---
|
||||
|
||||
- name: Add deploy user to docker group
|
||||
ansible.builtin.user:
|
||||
name: "{{ deploy_user }}"
|
||||
groups: docker
|
||||
append: true
|
||||
tags: docker
|
||||
|
||||
# --- Shared Traefik network ---
|
||||
|
||||
- name: Create shared Traefik Docker network
|
||||
community.docker.docker_network:
|
||||
name: "{{ traefik_network }}"
|
||||
driver: bridge
|
||||
state: present
|
||||
tags: docker
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"log-driver": "json-file",
|
||||
"log-opts": {
|
||||
"max-size": "10m",
|
||||
"max-file": "3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
- name: Restart gitea
|
||||
community.docker.docker_compose_v2:
|
||||
project_src: "{{ gitea_data_dir }}"
|
||||
state: present
|
||||
pull: missing
|
||||
recreate: always
|
||||
@@ -1,2 +1,38 @@
|
||||
---
|
||||
# TODO: implement gitea role
|
||||
- name: Create Gitea data directories
|
||||
ansible.builtin.file:
|
||||
path: "{{ item }}"
|
||||
state: directory
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0750"
|
||||
loop:
|
||||
- "{{ gitea_data_dir }}"
|
||||
- "{{ gitea_data_dir }}/data"
|
||||
- "{{ gitea_data_dir }}/db"
|
||||
tags: gitea
|
||||
|
||||
# Git-over-SSH runs on 2222 to avoid conflict with the host SSH on 22
|
||||
- name: Allow Gitea SSH (2222/tcp)
|
||||
community.general.ufw:
|
||||
rule: allow
|
||||
port: "2222"
|
||||
proto: tcp
|
||||
tags: gitea
|
||||
|
||||
- name: Deploy Docker Compose file
|
||||
ansible.builtin.template:
|
||||
src: docker-compose.yml.j2
|
||||
dest: "{{ gitea_data_dir }}/docker-compose.yml"
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
notify: Restart gitea
|
||||
tags: gitea
|
||||
|
||||
- name: Start Gitea
|
||||
community.docker.docker_compose_v2:
|
||||
project_src: "{{ gitea_data_dir }}"
|
||||
state: present
|
||||
pull: missing
|
||||
tags: gitea
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
# Managed by Ansible — do not edit manually
|
||||
services:
|
||||
gitea:
|
||||
image: gitea/gitea:{{ gitea_version }}
|
||||
container_name: gitea
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
# Database
|
||||
- GITEA__database__DB_TYPE=postgres
|
||||
- GITEA__database__HOST=gitea-db:5432
|
||||
- GITEA__database__NAME=gitea
|
||||
- GITEA__database__USER=gitea
|
||||
- GITEA__database__PASSWD={{ gitea_db_password }}
|
||||
# Server
|
||||
- GITEA__server__DOMAIN={{ domain_gitea }}
|
||||
- GITEA__server__ROOT_URL=https://{{ domain_gitea }}
|
||||
- GITEA__server__HTTP_PORT=3000
|
||||
- GITEA__server__SSH_DOMAIN={{ domain_gitea }}
|
||||
- GITEA__server__SSH_PORT=2222
|
||||
# Security
|
||||
- GITEA__security__SECRET_KEY={{ gitea_secret_key }}
|
||||
- GITEA__security__INTERNAL_TOKEN={{ gitea_internal_token }}
|
||||
# Disable public registration — set to false only for initial admin setup
|
||||
- GITEA__service__DISABLE_REGISTRATION={{ gitea_disable_registration | lower }}
|
||||
volumes:
|
||||
- {{ gitea_data_dir }}/data:/data
|
||||
ports:
|
||||
- "2222:22"
|
||||
networks:
|
||||
- traefik_public
|
||||
- gitea_internal
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.gitea.rule=Host(`{{ domain_gitea }}`)"
|
||||
- "traefik.http.routers.gitea.entrypoints=websecure"
|
||||
- "traefik.http.routers.gitea.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.services.gitea.loadbalancer.server.port=3000"
|
||||
depends_on:
|
||||
gitea-db:
|
||||
condition: service_healthy
|
||||
|
||||
gitea-db:
|
||||
image: postgres:{{ gitea_db_version }}
|
||||
container_name: gitea-db
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- POSTGRES_USER=gitea
|
||||
- POSTGRES_PASSWORD={{ gitea_db_password }}
|
||||
- POSTGRES_DB=gitea
|
||||
volumes:
|
||||
- {{ gitea_data_dir }}/db:/var/lib/postgresql/data
|
||||
networks:
|
||||
- gitea_internal
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U gitea"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
networks:
|
||||
traefik_public:
|
||||
external: true
|
||||
gitea_internal:
|
||||
internal: true
|
||||
@@ -0,0 +1,34 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>k8s.ladkau.de</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
background: #0f1117;
|
||||
color: #e2e8f0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
}
|
||||
.box {
|
||||
text-align: center;
|
||||
padding: 3rem;
|
||||
}
|
||||
h1 {
|
||||
font-size: 2rem;
|
||||
font-weight: 600;
|
||||
color: #f8fafc;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="box">
|
||||
<h1>k8s.ladkau.de</h1>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
- name: Restart k8s
|
||||
community.docker.docker_compose_v2:
|
||||
project_src: "{{ k8s_data_dir }}"
|
||||
state: present
|
||||
pull: missing
|
||||
recreate: always
|
||||
@@ -1,2 +1,39 @@
|
||||
---
|
||||
# TODO: implement k8s role
|
||||
# Placeholder — serves a static HTML page at k8s.ladkau.de
|
||||
- name: Create k8s placeholder directories
|
||||
ansible.builtin.file:
|
||||
path: "{{ item }}"
|
||||
state: directory
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0755"
|
||||
loop:
|
||||
- "{{ k8s_data_dir }}"
|
||||
- "{{ k8s_data_dir }}/html"
|
||||
tags: k8s
|
||||
|
||||
- name: Deploy placeholder HTML page
|
||||
ansible.builtin.copy:
|
||||
src: index.html
|
||||
dest: "{{ k8s_data_dir }}/html/index.html"
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
tags: k8s
|
||||
|
||||
- name: Deploy Docker Compose file
|
||||
ansible.builtin.template:
|
||||
src: docker-compose.yml.j2
|
||||
dest: "{{ k8s_data_dir }}/docker-compose.yml"
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
notify: Restart k8s
|
||||
tags: k8s
|
||||
|
||||
- name: Start k8s placeholder
|
||||
community.docker.docker_compose_v2:
|
||||
project_src: "{{ k8s_data_dir }}"
|
||||
state: present
|
||||
pull: missing
|
||||
tags: k8s
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
# Managed by Ansible — do not edit manually
|
||||
services:
|
||||
k8s-placeholder:
|
||||
image: nginx:alpine
|
||||
container_name: k8s-placeholder
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- {{ k8s_data_dir }}/html:/usr/share/nginx/html:ro
|
||||
networks:
|
||||
- traefik_public
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.k8s.rule=Host(`{{ domain_k8s }}`)"
|
||||
- "traefik.http.routers.k8s.entrypoints=websecure"
|
||||
- "traefik.http.routers.k8s.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.services.k8s.loadbalancer.server.port=80"
|
||||
|
||||
networks:
|
||||
traefik_public:
|
||||
external: true
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
- name: Restart mail
|
||||
community.docker.docker_compose_v2:
|
||||
project_src: "{{ mail_data_dir }}"
|
||||
state: present
|
||||
pull: missing
|
||||
recreate: always
|
||||
@@ -1,2 +1,30 @@
|
||||
---
|
||||
# TODO: implement mail role
|
||||
# Roundcube webmail client — connects to any external IMAP/SMTP server
|
||||
- name: Create Roundcube data directories
|
||||
ansible.builtin.file:
|
||||
path: "{{ item }}"
|
||||
state: directory
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0750"
|
||||
loop:
|
||||
- "{{ mail_data_dir }}"
|
||||
- "{{ mail_data_dir }}/db"
|
||||
tags: mail
|
||||
|
||||
- name: Deploy Docker Compose file
|
||||
ansible.builtin.template:
|
||||
src: docker-compose.yml.j2
|
||||
dest: "{{ mail_data_dir }}/docker-compose.yml"
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
notify: Restart mail
|
||||
tags: mail
|
||||
|
||||
- name: Start Roundcube
|
||||
community.docker.docker_compose_v2:
|
||||
project_src: "{{ mail_data_dir }}"
|
||||
state: present
|
||||
pull: missing
|
||||
tags: mail
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
# Managed by Ansible — do not edit manually
|
||||
services:
|
||||
mail-db:
|
||||
image: postgres:{{ roundcube_db_version }}
|
||||
container_name: mail-db
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_USER: roundcube
|
||||
POSTGRES_PASSWORD: "{{ roundcube_db_password }}"
|
||||
POSTGRES_DB: roundcube
|
||||
volumes:
|
||||
- {{ mail_data_dir }}/db:/var/lib/postgresql/data
|
||||
networks:
|
||||
- mail_internal
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U roundcube"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
roundcube:
|
||||
image: roundcube/roundcubemail:{{ roundcube_version }}
|
||||
container_name: roundcube
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
# Database
|
||||
ROUNDCUBEMAIL_DB_TYPE: pgsql
|
||||
ROUNDCUBEMAIL_DB_HOST: mail-db
|
||||
ROUNDCUBEMAIL_DB_PORT: "5432"
|
||||
ROUNDCUBEMAIL_DB_USER: roundcube
|
||||
ROUNDCUBEMAIL_DB_PASSWORD: "{{ roundcube_db_password }}"
|
||||
ROUNDCUBEMAIL_DB_NAME: roundcube
|
||||
# IMAP — leave empty to let users enter their own server at login,
|
||||
# or set to a specific host to lock it down (e.g. ssl://imap.example.com)
|
||||
ROUNDCUBEMAIL_DEFAULT_HOST: "{{ roundcube_imap_host }}"
|
||||
ROUNDCUBEMAIL_DEFAULT_PORT: "{{ roundcube_imap_port }}"
|
||||
# SMTP
|
||||
ROUNDCUBEMAIL_SMTP_SERVER: "{{ roundcube_smtp_host }}"
|
||||
ROUNDCUBEMAIL_SMTP_PORT: "{{ roundcube_smtp_port }}"
|
||||
# Security — 24-character random string used to encrypt session data
|
||||
ROUNDCUBEMAIL_DES_KEY: "{{ roundcube_des_key }}"
|
||||
ROUNDCUBEMAIL_UPLOAD_MAX_FILESIZE: 25M
|
||||
networks:
|
||||
- traefik_public
|
||||
- mail_internal
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.mail.rule=Host(`{{ domain_mail }}`)"
|
||||
- "traefik.http.routers.mail.entrypoints=websecure"
|
||||
- "traefik.http.routers.mail.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.services.mail.loadbalancer.server.port=80"
|
||||
depends_on:
|
||||
mail-db:
|
||||
condition: service_healthy
|
||||
|
||||
networks:
|
||||
traefik_public:
|
||||
external: true
|
||||
mail_internal:
|
||||
internal: true
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
- name: Restart nextcloud
|
||||
community.docker.docker_compose_v2:
|
||||
project_src: "{{ nextcloud_data_dir }}"
|
||||
state: present
|
||||
pull: missing
|
||||
recreate: always
|
||||
@@ -1,2 +1,31 @@
|
||||
---
|
||||
# TODO: implement nextcloud role
|
||||
- name: Create Nextcloud data directories
|
||||
ansible.builtin.file:
|
||||
path: "{{ item }}"
|
||||
state: directory
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0750"
|
||||
loop:
|
||||
- "{{ nextcloud_data_dir }}"
|
||||
- "{{ nextcloud_data_dir }}/html"
|
||||
- "{{ nextcloud_data_dir }}/db"
|
||||
- "{{ nextcloud_data_dir }}/redis"
|
||||
tags: nextcloud
|
||||
|
||||
- name: Deploy Docker Compose file
|
||||
ansible.builtin.template:
|
||||
src: docker-compose.yml.j2
|
||||
dest: "{{ nextcloud_data_dir }}/docker-compose.yml"
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
notify: Restart nextcloud
|
||||
tags: nextcloud
|
||||
|
||||
- name: Start Nextcloud
|
||||
community.docker.docker_compose_v2:
|
||||
project_src: "{{ nextcloud_data_dir }}"
|
||||
state: present
|
||||
pull: missing
|
||||
tags: nextcloud
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
# Managed by Ansible — do not edit manually
|
||||
services:
|
||||
nextcloud-db:
|
||||
image: postgres:{{ nextcloud_db_version }}
|
||||
container_name: nextcloud-db
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_USER: nextcloud
|
||||
POSTGRES_PASSWORD: "{{ nextcloud_db_password }}"
|
||||
POSTGRES_DB: nextcloud
|
||||
volumes:
|
||||
- {{ nextcloud_data_dir }}/db:/var/lib/postgresql/data
|
||||
networks:
|
||||
- nextcloud_internal
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U nextcloud"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
nextcloud-redis:
|
||||
image: redis:7-alpine
|
||||
container_name: nextcloud-redis
|
||||
restart: unless-stopped
|
||||
command: --save 60 1 --loglevel warning
|
||||
volumes:
|
||||
- {{ nextcloud_data_dir }}/redis:/data
|
||||
networks:
|
||||
- nextcloud_internal
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "redis-cli ping | grep PONG"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
nextcloud:
|
||||
image: nextcloud:{{ nextcloud_version }}
|
||||
container_name: nextcloud
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
# Database
|
||||
POSTGRES_HOST: nextcloud-db
|
||||
POSTGRES_DB: nextcloud
|
||||
POSTGRES_USER: nextcloud
|
||||
POSTGRES_PASSWORD: "{{ nextcloud_db_password }}"
|
||||
# Redis — used for file locking and memcache
|
||||
REDIS_HOST: nextcloud-redis
|
||||
# Admin bootstrap (only used on first start)
|
||||
NEXTCLOUD_ADMIN_USER: "{{ nextcloud_admin_user }}"
|
||||
NEXTCLOUD_ADMIN_PASSWORD: "{{ nextcloud_admin_password }}"
|
||||
# URLs — must match external domain; tells Nextcloud it's behind an HTTPS proxy
|
||||
NEXTCLOUD_TRUSTED_DOMAINS: "{{ domain_nextcloud }}"
|
||||
OVERWRITEPROTOCOL: https
|
||||
OVERWRITECLIURL: "https://{{ domain_nextcloud }}"
|
||||
OVERWRITEHOST: "{{ domain_nextcloud }}"
|
||||
# PHP limits for large file uploads
|
||||
PHP_MEMORY_LIMIT: 512M
|
||||
PHP_UPLOAD_LIMIT: 512M
|
||||
volumes:
|
||||
- {{ nextcloud_data_dir }}/html:/var/www/html
|
||||
networks:
|
||||
- traefik_public
|
||||
- nextcloud_internal
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.nextcloud.rule=Host(`{{ domain_nextcloud }}`)"
|
||||
- "traefik.http.routers.nextcloud.entrypoints=websecure"
|
||||
- "traefik.http.routers.nextcloud.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.services.nextcloud.loadbalancer.server.port=80"
|
||||
- "traefik.http.routers.nextcloud.middlewares=nextcloud-wellknown,nextcloud-headers"
|
||||
# Redirect .well-known CalDAV/CardDAV to the proper Nextcloud endpoint
|
||||
- "traefik.http.middlewares.nextcloud-wellknown.redirectregex.regex=^https://([^/]*)/.well-known/(card|cal)dav"
|
||||
- "traefik.http.middlewares.nextcloud-wellknown.redirectregex.replacement=https://$$1/remote.php/dav/"
|
||||
- "traefik.http.middlewares.nextcloud-wellknown.redirectregex.permanent=true"
|
||||
# Security headers recommended by Nextcloud
|
||||
- "traefik.http.middlewares.nextcloud-headers.headers.stsSeconds=15552000"
|
||||
- "traefik.http.middlewares.nextcloud-headers.headers.stsIncludeSubdomains=true"
|
||||
- "traefik.http.middlewares.nextcloud-headers.headers.stsPreload=true"
|
||||
depends_on:
|
||||
nextcloud-db:
|
||||
condition: service_healthy
|
||||
nextcloud-redis:
|
||||
condition: service_healthy
|
||||
|
||||
# Runs Nextcloud background jobs on a 5-minute schedule (replaces webcron/ajax cron)
|
||||
nextcloud-cron:
|
||||
image: nextcloud:{{ nextcloud_version }}
|
||||
container_name: nextcloud-cron
|
||||
restart: unless-stopped
|
||||
entrypoint: /cron.sh
|
||||
volumes:
|
||||
- {{ nextcloud_data_dir }}/html:/var/www/html
|
||||
networks:
|
||||
- nextcloud_internal
|
||||
depends_on:
|
||||
- nextcloud
|
||||
|
||||
networks:
|
||||
traefik_public:
|
||||
external: true
|
||||
nextcloud_internal:
|
||||
internal: true
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
- name: Restart registry
|
||||
community.docker.docker_compose_v2:
|
||||
project_src: "{{ registry_data_dir }}"
|
||||
state: present
|
||||
pull: missing
|
||||
recreate: always
|
||||
@@ -1,2 +1,41 @@
|
||||
---
|
||||
# TODO: implement registry role
|
||||
- name: Create registry data directories
|
||||
ansible.builtin.file:
|
||||
path: "{{ item }}"
|
||||
state: directory
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0750"
|
||||
loop:
|
||||
- "{{ registry_data_dir }}"
|
||||
- "{{ registry_data_dir }}/data"
|
||||
- "{{ registry_data_dir }}/auth"
|
||||
tags: registry
|
||||
|
||||
# htpasswd content is stored in vault and deployed as a file
|
||||
- name: Deploy htpasswd file
|
||||
ansible.builtin.copy:
|
||||
content: "{{ registry_htpasswd }}\n"
|
||||
dest: "{{ registry_data_dir }}/auth/htpasswd"
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0600"
|
||||
notify: Restart registry
|
||||
tags: registry
|
||||
|
||||
- name: Deploy Docker Compose file
|
||||
ansible.builtin.template:
|
||||
src: docker-compose.yml.j2
|
||||
dest: "{{ registry_data_dir }}/docker-compose.yml"
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
notify: Restart registry
|
||||
tags: registry
|
||||
|
||||
- name: Start registry
|
||||
community.docker.docker_compose_v2:
|
||||
project_src: "{{ registry_data_dir }}"
|
||||
state: present
|
||||
pull: missing
|
||||
tags: registry
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
# Managed by Ansible — do not edit manually
|
||||
services:
|
||||
registry:
|
||||
image: registry:2
|
||||
container_name: registry
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
REGISTRY_AUTH: htpasswd
|
||||
REGISTRY_AUTH_HTPASSWD_REALM: Container Registry
|
||||
REGISTRY_AUTH_HTPASSWD_PATH: /auth/htpasswd
|
||||
REGISTRY_STORAGE_DELETE_ENABLED: "true"
|
||||
volumes:
|
||||
- {{ registry_data_dir }}/data:/var/lib/registry
|
||||
- {{ registry_data_dir }}/auth:/auth:ro
|
||||
networks:
|
||||
- traefik_public
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.registry.rule=Host(`{{ domain_registry }}`)"
|
||||
- "traefik.http.routers.registry.entrypoints=websecure"
|
||||
- "traefik.http.routers.registry.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.services.registry.loadbalancer.server.port=5000"
|
||||
- "traefik.http.routers.registry.middlewares=registry-buffering"
|
||||
# Remove body size limit so large image layers can be pushed
|
||||
- "traefik.http.middlewares.registry-buffering.buffering.maxRequestBodyBytes=0"
|
||||
|
||||
networks:
|
||||
traefik_public:
|
||||
external: true
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
- name: Restart sso
|
||||
community.docker.docker_compose_v2:
|
||||
project_src: "{{ sso_data_dir }}"
|
||||
state: present
|
||||
pull: missing
|
||||
recreate: always
|
||||
@@ -1,2 +1,29 @@
|
||||
---
|
||||
# TODO: implement sso role
|
||||
- name: Create Keycloak data directories
|
||||
ansible.builtin.file:
|
||||
path: "{{ item }}"
|
||||
state: directory
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0750"
|
||||
loop:
|
||||
- "{{ sso_data_dir }}"
|
||||
- "{{ sso_data_dir }}/db"
|
||||
tags: sso
|
||||
|
||||
- name: Deploy Docker Compose file
|
||||
ansible.builtin.template:
|
||||
src: docker-compose.yml.j2
|
||||
dest: "{{ sso_data_dir }}/docker-compose.yml"
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
notify: Restart sso
|
||||
tags: sso
|
||||
|
||||
- name: Start Keycloak
|
||||
community.docker.docker_compose_v2:
|
||||
project_src: "{{ sso_data_dir }}"
|
||||
state: present
|
||||
pull: missing
|
||||
tags: sso
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
# Managed by Ansible — do not edit manually
|
||||
services:
|
||||
sso-db:
|
||||
image: postgres:{{ keycloak_db_version }}
|
||||
container_name: sso-db
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_USER: keycloak
|
||||
POSTGRES_PASSWORD: "{{ keycloak_db_password }}"
|
||||
POSTGRES_DB: keycloak
|
||||
volumes:
|
||||
- {{ sso_data_dir }}/db:/var/lib/postgresql/data
|
||||
networks:
|
||||
- sso_internal
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U keycloak"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
sso-keycloak:
|
||||
image: quay.io/keycloak/keycloak:{{ keycloak_version }}
|
||||
container_name: sso-keycloak
|
||||
command: start
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
# Database
|
||||
KC_DB: postgres
|
||||
KC_DB_URL: jdbc:postgresql://sso-db:5432/keycloak
|
||||
KC_DB_USERNAME: keycloak
|
||||
KC_DB_PASSWORD: "{{ keycloak_db_password }}"
|
||||
# Hostname — Traefik handles TLS, Keycloak listens plain HTTP internally
|
||||
KC_HOSTNAME: "{{ domain_sso }}"
|
||||
KC_HTTP_ENABLED: "true"
|
||||
KC_PROXY_HEADERS: xforwarded
|
||||
# Health endpoint (used by Docker healthcheck)
|
||||
KC_HEALTH_ENABLED: "true"
|
||||
# Bootstrap admin — only used on first start; change password via UI afterwards
|
||||
KEYCLOAK_ADMIN: "{{ keycloak_admin_user }}"
|
||||
KEYCLOAK_ADMIN_PASSWORD: "{{ keycloak_admin_password }}"
|
||||
networks:
|
||||
- traefik_public
|
||||
- sso_internal
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.sso.rule=Host(`{{ domain_sso }}`)"
|
||||
- "traefik.http.routers.sso.entrypoints=websecure"
|
||||
- "traefik.http.routers.sso.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.services.sso.loadbalancer.server.port=8080"
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -f http://localhost:8080/health/ready || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
start_period: 90s
|
||||
depends_on:
|
||||
sso-db:
|
||||
condition: service_healthy
|
||||
|
||||
networks:
|
||||
traefik_public:
|
||||
external: true
|
||||
sso_internal:
|
||||
internal: true
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
- name: Restart traefik
|
||||
community.docker.docker_compose_v2:
|
||||
project_src: "{{ traefik_data_dir }}"
|
||||
state: present
|
||||
pull: missing
|
||||
recreate: always
|
||||
@@ -1,2 +1,48 @@
|
||||
---
|
||||
# TODO: implement traefik role
|
||||
- name: Create Traefik data directory
|
||||
ansible.builtin.file:
|
||||
path: "{{ traefik_data_dir }}"
|
||||
state: directory
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0750"
|
||||
tags: traefik
|
||||
|
||||
# acme.json must be 0600 or Traefik refuses to start
|
||||
- name: Create acme.json for certificate storage
|
||||
ansible.builtin.file:
|
||||
path: "{{ traefik_data_dir }}/acme.json"
|
||||
state: touch
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0600"
|
||||
modification_time: preserve
|
||||
access_time: preserve
|
||||
tags: traefik
|
||||
|
||||
- name: Deploy Traefik static config
|
||||
ansible.builtin.template:
|
||||
src: traefik.yml.j2
|
||||
dest: "{{ traefik_data_dir }}/traefik.yml"
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
notify: Restart traefik
|
||||
tags: traefik
|
||||
|
||||
- name: Deploy Docker Compose file
|
||||
ansible.builtin.template:
|
||||
src: docker-compose.yml.j2
|
||||
dest: "{{ traefik_data_dir }}/docker-compose.yml"
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
notify: Restart traefik
|
||||
tags: traefik
|
||||
|
||||
- name: Start Traefik
|
||||
community.docker.docker_compose_v2:
|
||||
project_src: "{{ traefik_data_dir }}"
|
||||
state: present
|
||||
pull: missing
|
||||
tags: traefik
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
# Managed by Ansible — do not edit manually
|
||||
services:
|
||||
traefik:
|
||||
image: traefik:{{ traefik_version }}
|
||||
container_name: traefik
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
- {{ traefik_data_dir }}/traefik.yml:/traefik.yml:ro
|
||||
- {{ traefik_data_dir }}/acme.json:/acme.json
|
||||
networks:
|
||||
- traefik_public
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
# Dashboard — accessible at cloud.ladkau.de/dashboard/ protected by basic auth
|
||||
- "traefik.http.routers.dashboard.rule=Host(`{{ domain_cloud }}`) && (PathPrefix(`/api`) || PathPrefix(`/dashboard`))"
|
||||
- "traefik.http.routers.dashboard.entrypoints=websecure"
|
||||
- "traefik.http.routers.dashboard.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.routers.dashboard.service=api@internal"
|
||||
- "traefik.http.routers.dashboard.middlewares=dashboard-auth"
|
||||
- "traefik.http.middlewares.dashboard-auth.basicauth.users={{ traefik_dashboard_users }}"
|
||||
|
||||
networks:
|
||||
traefik_public:
|
||||
external: true
|
||||
@@ -0,0 +1,37 @@
|
||||
# Managed by Ansible — do not edit manually
|
||||
global:
|
||||
checkNewVersion: false
|
||||
sendAnonymousUsage: false
|
||||
|
||||
api:
|
||||
dashboard: true
|
||||
|
||||
log:
|
||||
level: INFO
|
||||
|
||||
accessLog: {}
|
||||
|
||||
entryPoints:
|
||||
web:
|
||||
address: ":80"
|
||||
http:
|
||||
redirections:
|
||||
entryPoint:
|
||||
to: websecure
|
||||
scheme: https
|
||||
permanent: true
|
||||
websecure:
|
||||
address: ":443"
|
||||
|
||||
providers:
|
||||
docker:
|
||||
exposedByDefault: false
|
||||
network: "{{ traefik_network }}"
|
||||
|
||||
certificatesResolvers:
|
||||
letsencrypt:
|
||||
acme:
|
||||
email: "{{ acme_email }}"
|
||||
storage: /acme.json
|
||||
httpChallenge:
|
||||
entryPoint: web
|
||||
Reference in New Issue
Block a user