In Parts 1 and 2, we covered server hardening and OpenStack infrastructure. Now we're diving into what most self-hosters spend the majority of their time on: deploying containerized applications.
Docker Compose files look simple until you need health checks, proper networking, volume permissions, restart policies, and reverse proxy integration all working together. Claude Code transforms this from documentation-hunting into conversation.
Prerequisites
- Claude Code installed (see Part 1)
- Docker and Docker Compose installed on your VPS
- Basic familiarity with containers
Quick Docker Installation
# Install Docker
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER
# Log out and back in, then verify
docker run hello-world
# Docker Compose is included with modern Docker
docker compose versionThe Compose Generation Workflow
The pattern is straightforward:
- Describe your stack in plain English
- Claude Code generates the compose file
- Review, refine, deploy
Let's work through progressively complex examples.
Example 1: Simple Single-Service Deployment
Start with something basic to see the workflow:
"Generate a Docker Compose file for Uptime Kuma (monitoring tool) with:
- • Persistent data volume
- • Restart unless stopped
- • Exposed on port 3001
- • Health check"
services:
uptime-kuma:
image: louislam/uptime-kuma:1
container_name: uptime-kuma
restart: unless-stopped
ports:
- "3001:3001"
volumes:
- uptime-kuma-data:/app/data
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3001/"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
volumes:
uptime-kuma-data:Simple, but notice Claude Code included a proper health check—something often forgotten in quick deployments.
Example 2: Multi-Service Stack with Networking
Now let's deploy something real—a complete Nextcloud setup:
"Create a production-ready Docker Compose for Nextcloud with:
- • Nextcloud (latest stable, Apache variant)
- • MariaDB for database
- • Redis for caching
- • All services on an internal network
- • Health checks on all services
- • Nextcloud waits for MariaDB and Redis to be healthy
- • Proper environment for reverse proxy"
version: '3.8'
services:
nextcloud:
image: nextcloud:stable-apache
container_name: nextcloud
restart: unless-stopped
ports:
- "8080:80"
environment:
- MYSQL_HOST=mariadb
- MYSQL_DATABASE=${MYSQL_DATABASE:-nextcloud}
- MYSQL_USER=${MYSQL_USER:-nextcloud}
- MYSQL_PASSWORD=${MYSQL_PASSWORD:?Database password required}
- REDIS_HOST=redis
- TRUSTED_PROXIES=172.16.0.0/12
- OVERWRITEPROTOCOL=https
volumes:
- nextcloud-html:/var/www/html
- nextcloud-data:/var/www/html/data
networks:
- nextcloud-internal
depends_on:
mariadb:
condition: service_healthy
redis:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost/status.php"]
interval: 30s
timeout: 10s
retries: 5
start_period: 120s
mariadb:
image: mariadb:11
container_name: nextcloud-db
restart: unless-stopped
environment:
- MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD:?Root password required}
- MYSQL_DATABASE=${MYSQL_DATABASE:-nextcloud}
- MYSQL_USER=${MYSQL_USER:-nextcloud}
- MYSQL_PASSWORD=${MYSQL_PASSWORD:?Database password required}
volumes:
- mariadb-data:/var/lib/mysql
networks:
- nextcloud-internal
command: >
--transaction-isolation=READ-COMMITTED
--log-bin=binlog
--binlog-format=ROW
healthcheck:
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
redis:
image: redis:7-alpine
container_name: nextcloud-redis
restart: unless-stopped
command: redis-server --maxmemory 256mb --maxmemory-policy allkeys-lru
volumes:
- redis-data:/data
networks:
- nextcloud-internal
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
networks:
nextcloud-internal:
driver: bridge
volumes:
nextcloud-html:
nextcloud-data:
mariadb-data:
redis-data:Notice what Claude Code included:
- MariaDB tuned for Nextcloud (transaction isolation, binlog format)
- Redis configured with memory limits and eviction policy
- Proper health check dependencies (Nextcloud waits for healthy DB/Redis)
- Environment variable validation with
${VAR:?error message}syntax
Example 3: Complete Stack with Traefik
Add automatic SSL and routing with Traefik:
"Add Traefik as a reverse proxy to the Nextcloud stack with:
- • Let's Encrypt SSL certificates (HTTP challenge)
- • Dashboard on traefik.mydomain.com with basic auth
- • HTTP to HTTPS redirect
- • Update Nextcloud service with Traefik labels"
traefik:
image: traefik:v3.0
container_name: traefik
restart: unless-stopped
command:
- --api.dashboard=true
- --providers.docker=true
- --providers.docker.exposedbydefault=false
- --providers.docker.network=traefik-public
- --entrypoints.web.address=:80
- --entrypoints.websecure.address=:443
- --entrypoints.web.http.redirections.entryPoint.to=websecure
- --certificatesresolvers.letsencrypt.acme.email=${ACME_EMAIL}
- --certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json
- --certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web
ports:
- 80:80
- 443:443
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- traefik-certs:/letsencrypt
networks:
- traefik-public
labels:
- "traefik.enable=true"
- "traefik.http.routers.dashboard.rule=Host(`${TRAEFIK_DASHBOARD_HOST}`)"
- "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_AUTH}"
healthcheck:
test: ["CMD", "traefik", "healthcheck"]
interval: 30s
timeout: 10s
retries: 3Nextcloud Traefik Labels
labels:
- "traefik.enable=true"
- "traefik.http.routers.nextcloud.rule=Host(`${NEXTCLOUD_HOST}`)"
- "traefik.http.routers.nextcloud.entrypoints=websecure"
- "traefik.http.routers.nextcloud.tls.certresolver=letsencrypt"
- "traefik.http.services.nextcloud.loadbalancer.server.port=80"
# Nextcloud-specific headers
- "traefik.http.routers.nextcloud.middlewares=nextcloud-headers,nextcloud-caldav"
- "traefik.http.middlewares.nextcloud-headers.headers.stsSeconds=31536000"
- "traefik.http.middlewares.nextcloud-caldav.redirectregex.permanent=true"
- "traefik.http.middlewares.nextcloud-caldav.redirectregex.regex=^https://(.*)/.well-known/ca(l|rd)dav"
- "traefik.http.middlewares.nextcloud-caldav.redirectregex.replacement=https://${1}/remote.php/dav/"Example 4: Development Stack Generation
Claude Code isn't just for production. Generate development environments instantly:
"Create a local development stack for a Node.js application with:
- • Node.js 20 with hot reload
- • PostgreSQL 16 with initial schema from ./init.sql
- • Redis for sessions
- • Mailpit for email testing
- • pgAdmin for database management"
services:
app:
image: node:20-alpine
container_name: dev-app
working_dir: /app
command: npm run dev
ports:
- "3000:3000"
environment:
- NODE_ENV=development
- DATABASE_URL=postgresql://${POSTGRES_USER:-dev}:${POSTGRES_PASSWORD:-devpass}@postgres:5432/${POSTGRES_DB:-devdb}
- REDIS_URL=redis://redis:6379
- SMTP_HOST=mailpit
- SMTP_PORT=1025
volumes:
- ./:/app
- /app/node_modules # Preserve node_modules in container
networks:
- dev-network
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
postgres:
image: postgres:16-alpine
container_name: dev-postgres
environment:
- POSTGRES_USER=${POSTGRES_USER:-dev}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-devpass}
- POSTGRES_DB=${POSTGRES_DB:-devdb}
ports:
- "5432:5432"
volumes:
- postgres-data:/var/lib/postgresql/data
- ./init.sql:/docker-entrypoint-initdb.d/init.sql:ro
networks:
- dev-network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-dev}"]
interval: 5s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
container_name: dev-redis
ports:
- "6379:6379"
networks:
- dev-network
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 5s
retries: 5
pgadmin:
image: dpage/pgadmin4:latest
container_name: dev-pgadmin
environment:
- PGADMIN_DEFAULT_EMAIL=admin@local.dev
- PGADMIN_DEFAULT_PASSWORD=admin
- PGADMIN_CONFIG_SERVER_MODE=False
ports:
- "5050:80"
networks:
- dev-network
mailpit:
image: axllent/mailpit:latest
container_name: dev-mailpit
ports:
- "8025:8025" # Web UI
- "1025:1025" # SMTP
networks:
- dev-network
networks:
dev-network:
driver: bridge
volumes:
postgres-data:Debugging Container Issues
When things go wrong, Claude Code helps diagnose problems:
"My Nextcloud container keeps restarting. Here's the output of docker logs nextcloud:
[paste logs]
And docker inspect nextcloud:
[paste relevant sections]
What's wrong?"
Claude Code analyzes the logs, identifies the issue (permissions, missing dependencies, configuration errors), and provides targeted fixes.
Common Issues Claude Code Can Diagnose
| Symptom | Typical Causes |
|---|---|
| Container restart loop | Health check failure, missing dependencies, permission errors |
| Network connectivity | Wrong network, DNS resolution, firewall rules |
| Volume permissions | UID/GID mismatch, SELinux contexts |
| Memory issues | OOM kills, no swap, misconfigured limits |
| Slow startup | Missing health check dependencies, race conditions |
Popular Self-Hosted Apps
Use these prompt patterns for popular applications:
Immich (Photo Management)
"Generate Docker Compose for Immich with PostgreSQL + pgvector, Redis, ML container for face recognition, persistent storage at /mnt/photos, Intel QuickSync transcoding, Traefik labels for photos.mydomain.com"
Vaultwarden (Password Manager)
"Create Docker Compose for Vaultwarden with SQLite, automatic backups every 6 hours, admin panel disabled, WebSocket support for live sync, Traefik labels with websocket config"
Paperless-ngx (Document Management)
"Docker Compose for Paperless-ngx with PostgreSQL, Redis task queue, Tika and Gotenberg for document processing, OCR for English and Spanish, Traefik routing"
Tips for Better Compose Generation
- Specify versions. "MariaDB 11" is better than "MariaDB" to avoid breaking changes.
- Describe the networking model. "Internal network for database, public network for Traefik" prevents confusion.
- Mention your reverse proxy. Traefik labels differ from Caddy or nginx-proxy configurations.
- Include operational requirements. Backups, logging, monitoring, and resource limits are easy to add in the initial generation.
Quick Reference: Compose Prompts
| Need | Prompt Pattern |
|---|---|
| New service | "Add [service] with [requirements] to the stack" |
| SSL/Routing | "Add Traefik labels for [domain] with Let's Encrypt" |
| Database | "Use [PostgreSQL/MariaDB] with persistent storage and health checks" |
| Development | "Create a dev compose with [services] and hot reload" |
| Debugging | "Container [name] shows [error]. Here are the logs: [logs]" |
| Migration | "Convert this docker run command to compose: [command]" |
What's Next
You can now generate complete container deployments through conversation. In Part 4, we'll cover Automated Server Monitoring & Alerting Pipelines—building observability stacks from scratch with Prometheus, Grafana, and custom alerting.
Continue to Part 4