Skip to content
Back to skills

Deploy App

ASecurity

Stand up a NEW containerized app or compose stack on a docker host. Use whenever the user wants to deploy, install, add, spin up, or "set up" a new container or stack (Jellyfin, Radarr, Sonarr, qBittorrent, Immich, a new arr app, a dashboard, etc.), or add a service to an existing stack. For fixing or operating an app that already exists, use docker-stack-ops instead; for updating images, use patch-management.

  • 80 stars
  • 0 votes
  • 0 copies
  • 3 views
  • Added September 5, 2026
ai-agentsrustgodockerapi

Works with

  • cli
  • api

Security analysis

A100/100

Scanned September 5, 2026

npx -y skills add serversathome/homelabhero --skill deploy-app --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Deploy App?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Deploy App
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/serversathome-deploy-app/badge)](https://www.skillsdirectory.com/skills/serversathome-deploy-app)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: deploy-app
description: >
  Stand up a NEW containerized app or compose stack on a docker host. Use
  whenever the user wants to deploy, install, add, spin up, or "set up" a new
  container or stack (Jellyfin, Radarr, Sonarr, qBittorrent, Immich, a new arr
  app, a dashboard, etc.), or add a service to an existing stack. For fixing or
  operating an app that already exists, use docker-stack-ops instead; for
  updating images, use patch-management.
---

# Deploy a new app / stack

Adding or starting containers is state-changing: propose the compose, confirm,
then bring it up.

## First, learn THIS homelab's layout - do not assume

Read `infra/docker-stacks.md` for the host alias, which compose model is in use,
the pool name, and the config/media paths. There are a few common structures and
this homelab may use any of them:

- **Single compose file** - one `/mnt/<pool>/docker/docker-compose.yml` holding
  every service; per-app config at `/mnt/<pool>/configs/<app>`; a single
  `/mnt/<pool>/media` mount.
- **Per-stack (Dockge)** - `/opt/stacks/<stack>/compose.yaml`, with the app's
  volumes kept inside the stack directory.
- **Portainer** - stacks pasted/edited in the Portainer UI.

If `infra/docker-stacks.md` does not say, ask which one before writing paths.

## A common convention worth knowing (the "servers@home" / TrueNAS *arr layout)

This is one widely used structure, not the only correct one - match what the box
actually uses:

- One `media` dataset with **subdirectories** `movies`, `tv`, `downloads` (a
  single mount so the *arr apps can **hardlink** instead of copying); one
  `configs` dataset with a **sub-dataset per app**.
- Volumes: `/mnt/<pool>/configs/<app>:/config` and `/mnt/<pool>/media:/media`.
- `PUID`/`PGID`: **568/568** on TrueNAS (the `apps` group); commonly **1000/1000**
  on Ubuntu/Proxmox hosts. Always set `TZ`. Dataset perms `770`, no spaces or
  capitals in names.
- Images: `lscr.io/linuxserver/<app>` or `ghcr.io/hotio/<app>`; `restart:
  unless-stopped`; ports as `HOST:CONTAINER`.

Service template:

    services:
      radarr:
        image: lscr.io/linuxserver/radarr:latest
        container_name: radarr
        environment:
          - PUID=568
          - PGID=568
          - TZ=America/New_York
        volumes:
          - /mnt/tank/configs/radarr:/config
          - /mnt/tank/media:/media
        ports:
          - 7878:7878
        restart: unless-stopped

## Download clients behind a VPN (killswitch)

Route a download client through a VPN container so it cannot leak the home IP.
The client defines **no ports of its own** - the ports go on the VPN container,
and the client joins its network. No `depends_on` is needed.

    services:
      gluetun:                       # or a linuxserver/wireguard container
        image: qmcgaw/gluetun
        cap_add: [NET_ADMIN]
        environment:
          - VPN_SERVICE_PROVIDER=...
          - VPN_TYPE=wireguard
          - WIREGUARD_PRIVATE_KEY=...
          - FIREWALL_VPN_INPUT_PORTS=6881
        ports:
          - 8080:8080               # the qBittorrent WebUI port lives HERE
          - 6881:6881/tcp
      qbittorrent:
        image: lscr.io/linuxserver/qbittorrent:latest
        network_mode: "service:gluetun"   # shares the VPN's network stack
        # NO ports: block here

After it's up, confirm the egress IP is the VPN's, not home (see the gluetun rule
in docker-stack-ops).

## Workflow

1. Confirm app, image, ports, and the layout/paths from `infra/docker-stacks.md`.
2. Create the config dir/dataset with the right owner and `770` (on TrueNAS,
   create the dataset in the UI or `mkdir` under the configs dataset).
3. Add the service block (single file) or create the stack dir (Dockge/Portainer).
   Show the user the compose and confirm before applying.
4. Bring it up (state-changing):

       hh run <dockerhost> "cd <composedir> && docker compose up -d <service>"

5. Verify: `docker ps`, `docker logs --tail 80 <name>`, and the
   WebUI on its port.
6. External access wanted? Add it to the Cloudflare Tunnel (Zero Trust -> the
   tunnel -> add public hostname) or the reverse proxy - see network-diag and
   `infra/`. Do not publish anything externally without an explicit go-ahead.

Record the new app in `infra/docker-stacks.md` so the picture stays current.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.