Deployment guides
Sections
This page contains ready-to-use examples for deploying popular applications
with the debops.docker_service role. Each example includes the complete
service definition, nginx reverse proxy configuration, and a
container healthcheck where the application supports it.
The examples assume that the target host already has Docker Engine installed
via the debops.docker_server role and that the host is a member of both
the [debops_service_docker_server] and [debops_service_docker_service]
Ansible inventory groups.
Bugsink
Bugsink is a Sentry-compatible error tracking server. This example deploys Bugsink with a PostgreSQL database managed by the debops.postgresql_server role on the same host, connecting through a UNIX socket.
Prerequisites
The host must be a member of the following inventory groups:
[debops_service_postgresql_server]
bugsink.example.com
[debops_service_docker_server]
bugsink.example.com
[debops_service_docker_service]
bugsink.example.com
The [debops_service_postgresql_server] group ensures that PostgreSQL is
installed before the docker_service playbook runs. The postgresql block
in the service definition will automatically create the database and user
through the debops.postgresql role.
Service definition
# ansible/inventory/host_vars/bugsink.example.com/docker_service.yml
---
bugsink_secret_key: "{{ lookup('password', secret
+ '/bugsink/secret_key
length=50 chars=ascii_letters,digits') }}"
bugsink__postgresql_password: "{{ lookup('password', secret
+ '/postgresql/'
+ postgresql__password_hostname + '/'
+ postgresql__port
+ '/credentials/bugsink/password
length=' + postgresql__password_length
+ ' chars=' + postgresql__password_characters) }}"
docker_service__host_services:
- name: 'bugsink'
image: 'bugsink/bugsink:latest'
restart_policy: 'unless-stopped'
ports:
- '127.0.0.1:8000:8000'
volumes:
- '/srv/docker/bugsink/data:/data'
- '/var/run/postgresql:/var/run/postgresql:ro'
postgresql:
database: 'bugsink'
user: 'bugsink'
env:
SECRET_KEY: '{{ bugsink_secret_key }}'
DATABASE_URL: 'postgresql://bugsink:{{ bugsink__postgresql_password }}@/bugsink?host=/var/run/postgresql'
PORT: '8000'
BEHIND_HTTPS_PROXY: 'True'
nginx:
enabled: true
fqdn: 'bugsink.example.com'
port: '8000'
The SECRET_KEY and PostgreSQL password are auto-generated and stored in the
DebOps secret/ directory. The bugsink__postgresql_password lookup
uses the same path as the debops.postgresql role, ensuring the password
in DATABASE_URL matches the one set in the database.
The PostgreSQL UNIX socket directory is mounted read-only into the container.
The DATABASE_URL uses the host=/var/run/postgresql query parameter to
connect through the socket instead of TCP.
Creating the initial superuser
After the first deployment, create an admin account by running:
$ docker exec -it bugsink bugsink-manage createsuperuser
The DATABASE_URL environment variable is already set in the container, so
the management command will connect to the correct database automatically.
Note
Bugsink does not currently provide an official health check endpoint (issue #98). A basic HTTP check can be used as a workaround once the endpoint is available.
Grafana
Grafana is a popular observability dashboard. This example deploys Grafana with persistent storage, auto-generated admin password, and an nginx reverse proxy.
Service definition
# ansible/inventory/host_vars/grafana.example.com/docker_service.yml
docker_service__host_services:
- name: 'grafana'
image: 'grafana/grafana:11.0.0'
ports:
- '127.0.0.1:3000:3000'
volumes:
- '/srv/docker/grafana/data:/var/lib/grafana'
env:
GF_SERVER_ROOT_URL: 'https://grafana.example.com'
GF_SECURITY_ADMIN_PASSWORD: '{{ lookup("password", secret
+ "/docker_service/grafana/admin_password") }}'
memory: '512m'
healthcheck:
test: [ 'CMD-SHELL',
'wget --no-verbose --tries=1 --spider http://127.0.0.1:3000/api/health || exit 1' ]
interval: '15s'
timeout: '5s'
retries: 3
start_period: '30s'
nginx:
enabled: true
fqdn: 'grafana.example.com'
port: '3000'
proxy_options: |
proxy_buffering off;
Datasource provisioning
Grafana supports automatic datasource configuration through provisioning
files. Use config_files with the src parameter to render a Jinja2
template:
- name: 'grafana'
image: 'grafana/grafana:11.0.0'
config_files:
- dest: '/srv/docker/grafana/provisioning/datasources/victoria.yml'
src: 'grafana/datasources.yml.j2'
volumes:
- '/srv/docker/grafana/data:/var/lib/grafana'
- '/srv/docker/grafana/provisioning:/etc/grafana/provisioning:ro'
# ... remaining parameters ...
The template file (e.g.
ansible/docker_service/by-host/hostname/grafana/datasources.yml.j2)
might look like:
apiVersion: 1
datasources:
- name: VictoriaMetrics
type: prometheus
access: proxy
url: http://127.0.0.1:8428
isDefault: true
Homepage
Homepage is a modern application dashboard with
integrations for over 100 services. This example uses config_dir to manage
multiple configuration files from a template directory.
Service definition
# ansible/inventory/host_vars/dashboard.example.com/docker_service.yml
docker_service__host_services:
- name: 'homepage'
image: 'ghcr.io/gethomepage/homepage:latest'
ports:
- '127.0.0.1:3000:3000'
config_dir:
src: 'homepage/config'
dest: '/srv/docker/homepage/config'
volumes:
- '/srv/docker/homepage/config:/app/config:ro'
env:
HOMEPAGE_ALLOWED_HOSTS: 'dashboard.example.com'
healthcheck:
test: [ 'CMD-SHELL',
'wget --no-verbose --tries=1 --spider http://127.0.0.1:3000/api/healthcheck || exit 1' ]
interval: '15s'
timeout: '5s'
retries: 3
start_period: '20s'
nginx:
enabled: true
fqdn: 'dashboard.example.com'
port: '3000'
Template directory
Create the configuration templates in your Ansible resources directory. The
config_dir parameter scans the source directory using
community.general.filetree and renders each file as a Jinja2 template:
ansible/docker_service/by-host/dashboard.example.com/
homepage/config/
settings.yaml
services.yaml
bookmarks.yaml
widgets.yaml
Example settings.yaml:
---
title: My Dashboard
theme: dark
color: slate
headerStyle: clean
Example services.yaml:
---
- Infrastructure:
- Proxmox:
href: https://proxmox.example.com
icon: proxmox.svg
description: Hypervisor
- Grafana:
href: https://grafana.example.com
icon: grafana.svg
description: Monitoring dashboard
Example bookmarks.yaml:
---
- Development:
- GitLab:
- abbr: GL
href: https://gitlab.example.com
Example widgets.yaml:
---
- resources:
cpu: true
memory: true
disk: /
- search:
provider: duckduckgo
target: _blank
Important
All Homepage configuration files must be provided through config_dir.
Because the volume is mounted read-only (:ro), the container cannot
create missing files at startup. If any expected file (e.g.
settings.yaml, services.yaml, bookmarks.yaml, widgets.yaml,
docker.yaml, custom.css, custom.js) is absent from the template
directory, Homepage will fail to start. Make sure every file that Homepage
expects is present in the source directory, even if it only contains an
empty YAML document (---).
Note
Homepage requires the HOMEPAGE_ALLOWED_HOSTS environment variable to
be set when accessed through a reverse proxy. Set it to the FQDN used in
the nginx configuration.
Vaultwarden
Vaultwarden is a lightweight, Bitwarden-compatible password manager server. It uses SQLite by default, so no external database is required.
Service definition
# ansible/inventory/host_vars/vault.example.com/docker_service.yml
docker_service__host_services:
- name: 'vaultwarden'
image: 'vaultwarden/server:latest'
ports:
- '127.0.0.1:8080:80'
volumes:
- '/srv/docker/vaultwarden/data:/data'
env:
DOMAIN: 'https://vault.example.com'
SIGNUPS_ALLOWED: 'false'
ADMIN_TOKEN: '{{ lookup("password", secret
+ "/docker_service/vaultwarden/admin_token
chars=ascii_letters,digits length=48") }}'
healthcheck:
test: [ 'CMD-SHELL',
'wget --no-verbose --tries=1 --spider http://127.0.0.1:80/alive || exit 1' ]
interval: '30s'
timeout: '5s'
retries: 3
start_period: '15s'
nginx:
enabled: true
fqdn: 'vault.example.com'
port: '8080'
options: |
client_max_body_size 128m;
SMTP configuration
To enable email notifications (password reset, 2FA, invitations), add SMTP environment variables:
env:
DOMAIN: 'https://vault.example.com'
SIGNUPS_ALLOWED: 'false'
ADMIN_TOKEN: '{{ lookup("password", secret
+ "/docker_service/vaultwarden/admin_token
chars=ascii_letters,digits length=48") }}'
SMTP_HOST: 'smtp.example.com'
SMTP_FROM: 'vaultwarden@example.com'
SMTP_PORT: '587'
SMTP_SECURITY: 'starttls'
SMTP_USERNAME: 'vaultwarden@example.com'
SMTP_PASSWORD: '{{ lookup("password", secret
+ "/docker_service/vaultwarden/smtp_password") }}'
VictoriaMetrics
VictoriaMetrics is a fast, cost-effective time series database. This is a simple single-node deployment with an nginx reverse proxy providing TLS termination.
Inventory
[debops_all_hosts]
vmetrics.example.com
[debops_service_docker_server]
vmetrics.example.com
[debops_service_docker_service]
vmetrics.example.com
Service definition
# ansible/inventory/host_vars/vmetrics.example.com/docker_service.yml
docker_service__host_services:
- name: 'victoriametrics'
image: 'victoriametrics/victoria-metrics:v1.93.0'
ports:
- '127.0.0.1:8428:8428'
volumes:
- '/srv/docker/victoriametrics/data:/victoria-metrics-data'
command: '-retentionPeriod=12 -selfScrapeInterval=10s'
memory: '512m'
healthcheck:
test: [ 'CMD-SHELL',
'wget --no-verbose --tries=1 --spider http://127.0.0.1:8428/health || exit 1' ]
interval: '15s'
timeout: '5s'
retries: 3
start_period: '10s'
nginx:
enabled: true
fqdn: 'vmetrics.example.com'
port: '8428'
The -retentionPeriod=12 flag configures a 12-month data retention period.
The -selfScrapeInterval=10s flag enables internal metrics collection.
The container binds to 127.0.0.1 only -- external access is provided by
nginx with TLS termination handled by the DebOps PKI
infrastructure.
VictoriaMetrics with vmauth
vmauth is an HTTP proxy/authenticator for VictoriaMetrics. It provides authentication (Basic Auth, Bearer tokens) and request routing. This example deploys VictoriaMetrics together with vmauth as an authentication layer.
Two architectures are possible:
vmauth behind nginx (recommended) -- nginx handles TLS termination; vmauth handles authentication and routes to VictoriaMetrics.
vmauth with own TLS -- vmauth terminates TLS directly using DebOps PKI certificates mounted from the host.
This guide demonstrates the recommended approach (vmauth behind nginx).
Service definition
# ansible/inventory/host_vars/vmetrics.example.com/docker_service.yml
docker_service__host_services:
- name: 'victoriametrics'
image: 'victoriametrics/victoria-metrics:v1.93.0'
ports:
- '127.0.0.1:8428:8428'
volumes:
- '/srv/docker/victoriametrics/data:/victoria-metrics-data'
command: '-retentionPeriod=12 -selfScrapeInterval=10s'
healthcheck:
test: [ 'CMD-SHELL',
'wget --no-verbose --tries=1 --spider http://127.0.0.1:8428/health || exit 1' ]
interval: '15s'
timeout: '5s'
retries: 3
start_period: '10s'
- name: 'vmauth'
image: 'victoriametrics/vmauth:latest'
ports:
- '127.0.0.1:8427:8427'
config_files:
- dest: '/srv/docker/vmauth/auth.yml'
content: |
users:
- username: 'metrics'
password: '{{ lookup("password", secret
+ "/docker_service/vmauth/metrics_password") }}'
url_prefix: 'http://{{ ansible_default_ipv4.address | d("127.0.0.1") }}:8428/'
mode: '0640'
volumes:
- '/srv/docker/vmauth/auth.yml:/etc/vmauth/auth.yml:ro'
command: '-auth.config=/etc/vmauth/auth.yml'
healthcheck:
test: [ 'CMD-SHELL',
'wget --no-verbose --tries=1 --spider http://127.0.0.1:8427/health || exit 1' ]
interval: '15s'
timeout: '5s'
retries: 3
start_period: '10s'
nginx:
enabled: true
fqdn: 'vmetrics.example.com'
port: '8427'
In this setup, VictoriaMetrics does not have an nginx block -- it is only
accessible through vmauth. The nginx reverse proxy terminates TLS
and forwards traffic to vmauth, which authenticates requests before proxying
them to VictoriaMetrics.
The config_files parameter generates the vmauth configuration file with
an auto-generated password stored in the DebOps secret directory. The file is
mounted read-only into the container.
Alternative: vmauth with own TLS
If you prefer vmauth to handle TLS directly (without nginx), mount the DebOps PKI certificates into the container:
- name: 'vmauth'
image: 'victoriametrics/vmauth:latest'
init: true
ports:
- '0.0.0.0:443:443'
config_files:
- dest: '/srv/docker/vmauth/auth.yml'
content: |
users:
- username: 'metrics'
password: '{{ lookup("password", secret
+ "/docker_service/vmauth/metrics_password") }}'
url_prefix: 'http://{{ ansible_default_ipv4.address | d("127.0.0.1") }}:8428/'
mode: '0640'
volumes:
- '/srv/docker/vmauth/auth.yml:/etc/vmauth/auth.yml:ro'
- '/etc/pki/realms/domain/default.crt:/etc/vmauth/cert.pem:ro'
- '/etc/pki/realms/domain/default.key:/etc/vmauth/key.pem:ro'
command: >-
-auth.config=/etc/vmauth/auth.yml
-tls -tlsCertFile=/etc/vmauth/cert.pem -tlsKeyFile=/etc/vmauth/key.pem
-httpListenAddr=:443 -httpInternalListenAddr=:8427
healthcheck:
test: [ 'CMD-SHELL',
'wget --no-check-certificate --no-verbose --tries=1 --spider https://127.0.0.1:8427/health || exit 1' ]
interval: '15s'
timeout: '5s'
retries: 3
start_period: '10s'
The certificate paths (/etc/pki/realms/domain/) are managed by the
debops.pki role. vmauth automatically reloads certificates when they
change on disk, so no container restart is needed for certificate renewal.
Important
The init: true parameter is required when running vmauth with TLS
healthchecks. BusyBox wget (used in Alpine-based VictoriaMetrics
images) spawns a helper ssl_client process for each HTTPS request.
Without an init process to reap these children, they accumulate as zombie
processes because vmauth (PID 1) does not handle SIGCHLD.