SaltStack on Your VPS Series
    Part 5 of 6

    Docker & Container Management with Salt

    Install Docker via Salt, manage containers as state, deploy Compose stacks, and automate cleanup across your fleet.

    35 minutes

    Installing Docker via Salt States

    /srv/salt/docker/install.sls
    remove_old_docker:
      pkg.removed:
        - pkgs:
          - docker
          - docker-engine
          - docker.io
          - containerd
          - runc
    
    docker_prerequisites:
      pkg.installed:
        - pkgs:
          - ca-certificates
          - curl
          - gnupg
          - lsb-release
    
    docker_gpg_key:
      cmd.run:
        - name: |
            curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
              | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
        - creates: /usr/share/keyrings/docker-archive-keyring.gpg
    
    docker_packages:
      pkg.installed:
        - pkgs:
          - docker-ce
          - docker-ce-cli
          - containerd.io
          - docker-buildx-plugin
          - docker-compose-plugin
    
    docker_service:
      service.running:
        - name: docker
        - enable: True
        - require:
          - pkg: docker_packages

    Docker Daemon Configuration

    /srv/salt/docker/files/daemon.json
    {
      "log-driver": "json-file",
      "log-opts": {
        "max-size": "10m",
        "max-file": "3"
      },
      "storage-driver": "overlay2",
      "live-restore": true,
      "userland-proxy": false
    }

    Key settings: log-opts prevents logs from consuming all disk, live-restore keeps containers running during daemon upgrades.

    Docker Group Users

    /srv/salt/docker/users.sls
    {% set docker_users = pillar.get('docker:users', []) %}
    
    {% for username in docker_users %}
    add_{{ username }}_to_docker:
      user.present:
        - name: {{ username }}
        - groups:
          - docker
        - require:
          - pkg: docker_packages
    {% endfor %}

    Managing Containers

    Ad-hoc Commands

    sudo salt 'web-01' docker.ps
    sudo salt 'web-01' docker.pull nginx:alpine
    sudo salt 'web-01' docker.images
    sudo salt 'web-01' docker.logs nginx-test

    Declarative Container State

    nginx_image:
      docker_image.present:
        - name: nginx:1.25-alpine
    
    nginx_container:
      docker_container.running:
        - name: nginx-app
        - image: nginx:1.25-alpine
        - detach: True
        - restart_policy: always
        - port_bindings:
          - 80:80
          - 443:443
        - binds:
          - /var/www/html:/usr/share/nginx/html:ro
        - require:
          - docker_image: nginx_image

    Docker Compose Stacks

    /srv/salt/docker/compose.sls
    compose_directory:
      file.directory:
        - name: /opt/apps/myapp
        - user: deploy
        - group: docker
        - mode: '0755'
        - makedirs: True
    
    myapp_compose_file:
      file.managed:
        - name: /opt/apps/myapp/docker-compose.yml
        - source: salt://docker/files/myapp-compose.yml
        - user: deploy
        - group: docker
    
    myapp_env_file:
      file.managed:
        - name: /opt/apps/myapp/.env
        - source: salt://docker/files/myapp.env
        - mode: '0600'
    
    myapp_up:
      cmd.run:
        - name: docker compose up -d --remove-orphans
        - cwd: /opt/apps/myapp
        - onchanges:
          - file: myapp_compose_file
          - file: myapp_env_file

    Networks & Volumes

    myapp_network:
      docker_network.present:
        - name: myapp_network
        - driver: bridge
    
    db_data_volume:
      docker_volume.present:
        - name: myapp_db_data
        - driver: local

    Automated Image Updates

    {% set images = pillar.get('docker:managed_images', []) %}
    
    {% for image in images %}
    pull_{{ image | replace('/', '_') | replace(':', '_') }}:
      docker_image.present:
        - name: {{ image }}
        - force: True
    {% endfor %}
    sudo salt -G 'role:webserver' state.apply docker.update

    Container Health Monitoring

    sudo salt 'web-*' cmd.run 'docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"'
    sudo salt 'web-*' cmd.run 'docker ps --filter health=unhealthy --format "{{.Names}}"'
    sudo salt 'web-*' cmd.run 'docker system df'

    Cleanup States

    /srv/salt/docker/cleanup.sls
    docker_prune_cron:
      cron.present:
        - name: docker system prune -f --filter "until=168h"
        - user: root
        - minute: 0
        - hour: 3
        - dayweek: 0
        - comment: Weekly Docker resource cleanup
    
    docker_prune_images:
      cmd.run:
        - name: docker image prune -f
        - onlyif: test $(docker images -f dangling=true -q | wc -l) -gt 10

    Combining LAMP & Docker

    Run Nginx on the host as a reverse proxy with Dockerized application containers behind it:

    # top.sls
    base:
      'web-*':
        - lamp.webserver    # host Nginx as reverse proxy
        - docker            # Docker for app containers
        - lamp.vhost        # vhost configs pointing to container ports
    Nginx reverse proxy to container
    server {
        listen 80;
        server_name app.example.com;
    
        location / {
            proxy_pass http://127.0.0.1:8080;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        }
    }

    What's Next

    Docker is now fully managed by Salt. In the final part, we cover Pillars for secrets management, Grains for advanced targeting, orchestration for sequenced deployments, and security hardening states for every server.