Automated infrastructure provisioning and deployment for the ICU (Intensive Care Unit) Application stack using Ansible. This setup deploys a complete production environment with Docker containers, reverse proxy, and automatic HTTPS.
- Architecture Overview
- Prerequisites
- Customization Guide
- Quick Start
- Project Structure
- Configuration
- Usage & Commands
- CI/CD Integration
- Roles Documentation
- Security Best Practices
- What Gets Deployed
This Ansible automation deploys a full-stack application on a single DigitalOcean droplet with the following architecture:
Internet
|
v
[Caddy Reverse Proxy] (Automatic HTTPS)
|
+---> Frontend (icu.darwishathibi.com) --> SvelteKit App (Port 5173)
|
+---> Backend (api-icu.darwishathibi.com) --> ASP.NET Core API (Port 5141)
|
v
MySQL 8.0 (Port 3306)
Technology Stack:
- Frontend: SvelteKit application (containerized)
- Backend: ASP.NET Core API (containerized)
- Database: MySQL 8.0 (containerized)
- Reverse Proxy: Caddy web server with automatic Let's Encrypt SSL
- Container Runtime: Docker with Docker Compose
- Firewall: UFW (Uncomplicated Firewall)
Default Target Infrastructure (customizable):
- Server: DigitalOcean Droplet (188.166.249.118) - Configure in inventory/hosts.yml
- Organization: Deta-Technologies - Configure in group_vars/all.yml
- Container Registry: GitHub Container Registry (ghcr.io)
Note: These are default values. See the Customization Guide to deploy to your own infrastructure.
-
Ansible installed (version 2.9 or higher)
# Install on macOS brew install ansible # Install on Ubuntu/Debian sudo apt update && sudo apt install ansible
-
SSH access configured with key-based authentication
# Generate SSH key if you don't have one ssh-keygen -t ed25519 -C "your_email@example.com" # Copy public key to server ssh-copy-id -i ~/.ssh/id_ed25519 root@188.166.249.118
-
Environment variables set with sensitive credentials
export MYSQL_ROOT_PASSWORD="your_secure_root_password" export MYSQL_PASSWORD="your_database_password" export GITHUB_TOKEN="ghp_your_github_personal_access_token" export GITHUB_USERNAME="your_github_username"
- Operating System: Ubuntu 20.04/22.04 or Debian-based system
- Access: Root access or sudo privileges
- Network: SSH access enabled (port 22)
- Connectivity: Internet access for package downloads
-
DNS Configuration: Your domains must point to your server IP
- Example:
icu.darwishathibi.com→ 188.166.249.118 - Example:
api-icu.darwishathibi.com→ 188.166.249.118 - See Customization Guide for your own domains
- Example:
-
GitHub Container Registry: Access to private Docker images
- Images hosted at
ghcr.io/deta-technologies/ - Requires valid GitHub token with
read:packagespermission
- Images hosted at
-
Let's Encrypt: Automatic SSL requires DNS to be properly configured
This section is for users who want to deploy to their own infrastructure with different domains, IP addresses, and ports.
The current configuration deploys to:
- Server IP: 188.166.249.118
- Frontend Domain: icu.darwishathibi.com (Port 5173)
- Backend Domain: api-icu.darwishathibi.com (Port 5141)
To customize for your environment, you need to modify 2 main files and configure DNS settings.
File: inventory/hosts.yml
What to change:
all:
hosts:
icu_server:
ansible_host: 188.166.249.118 # ← CHANGE THIS to your server IP
ansible_user: root # ← Change if using different user
ansible_ssh_private_key_file: ~/.ssh/id_ed25519 # ← Update SSH key path
ansible_python_interpreter: /usr/bin/python3Example for your own server:
all:
hosts:
icu_server:
ansible_host: 203.0.113.50 # Your DigitalOcean/AWS/VPS IP
ansible_user: ubuntu # or root, admin, etc.
ansible_ssh_private_key_file: ~/.ssh/my_key
ansible_python_interpreter: /usr/bin/python3File: group_vars/all.yml
What to change:
# Domain Configuration
frontend_domain: "icu.darwishathibi.com" # ← CHANGE THIS to your domain
backend_domain: "api-icu.darwishathibi.com" # ← CHANGE THIS to your API domain
# Port Configuration (optional - only change if your app uses different ports)
frontend_port: 5173 # ← Change if your frontend uses different port
backend_port: 5141 # ← Change if your backend uses different port
# Database Configuration
mysql_database: "icu_db" # ← Change database name if desired
mysql_user: "mandur_gede" # ← Change database user if desired
# Application Configuration
app_directory: "/opt/icu_application" # ← Change deployment path if desired
github_org: "Deta-Technologies" # ← CHANGE THIS to your GitHub org/usernameExample for your own setup:
# Domain Configuration
frontend_domain: "myapp.example.com"
backend_domain: "api.myapp.example.com"
# Port Configuration
frontend_port: 3000 # If using Next.js or different port
backend_port: 8080 # If using different backend port
# Database Configuration
mysql_database: "myapp_production"
mysql_user: "myapp_user"
# Application Configuration
app_directory: "/opt/myapp"
github_org: "your-github-username"Before running the playbook, ensure your domains point to your server:
Required DNS A Records:
myapp.example.com → 203.0.113.50 (your server IP)
api.myapp.example.com → 203.0.113.50 (your server IP)
How to verify DNS:
# Check if DNS is properly configured
dig myapp.example.com +short
# Should return: 203.0.113.50
dig api.myapp.example.com +short
# Should return: 203.0.113.50Note: DNS propagation can take 5 minutes to 48 hours. Let's Encrypt SSL will only work after DNS is properly configured.
If you're using your own Docker images (not Deta-Technologies images), you'll need to update the image references:
File: roles/application/templates/docker-compose.yml.j2
Look for these lines and update:
services:
backend:
image: ghcr.io/{{ github_org }}/icu-backend:latest # Auto-uses your github_org
# This becomes: ghcr.io/your-username/icu-backend:latest
frontend:
image: ghcr.io/{{ github_org }}/icu-frontend:latest # Auto-uses your github_org
# This becomes: ghcr.io/your-username/icu-frontend:latestThe images will automatically use your github_org variable, so you only need to ensure:
- Your images are pushed to GitHub Container Registry
- Your images are named
icu-backendandicu-frontend(or update the template)
If using different image names:
# Change from:
image: ghcr.io/{{ github_org }}/icu-backend:latest
# To (example):
image: ghcr.io/{{ github_org }}/my-api-service:latestUse this checklist to ensure you've customized everything:
- Updated
inventory/hosts.ymlwith your server IP - Updated
group_vars/all.ymlwith your domains - Updated
group_vars/all.ymlwith your GitHub organization - Configured DNS A records for both domains
- Verified DNS propagation with
digcommand - Set up SSH key-based authentication to your server
- Set environment variables with your credentials
- Updated ports in
group_vars/all.yml(if different) - Updated app_directory path (if desired)
- Updated GitHub image names (if using different names)
If you want to manage multiple environments (staging, production), create separate inventory files:
Directory structure:
inventory/
├── production.yml
└── staging.yml
Example inventory/staging.yml:
all:
hosts:
staging_server:
ansible_host: 198.51.100.10
ansible_user: root
ansible_ssh_private_key_file: ~/.ssh/id_ed25519
ansible_python_interpreter: /usr/bin/python3Example inventory/production.yml:
all:
hosts:
production_server:
ansible_host: 203.0.113.50
ansible_user: root
ansible_ssh_private_key_file: ~/.ssh/id_ed25519
ansible_python_interpreter: /usr/bin/python3Then use different variable files:
group_vars/
├── all.yml # Shared variables
├── staging.yml # Staging-specific variables
└── production.yml # Production-specific variables
Deploy to specific environment:
# Deploy to staging
ansible-playbook -i inventory/staging.yml playbook.yml
# Deploy to production
ansible-playbook -i inventory/production.yml playbook.ymlNo changes needed to the playbook! Just update:
inventory/hosts.ymlwith your cloud VM's IP- DNS records to point to that IP
- SSH key to match your cloud provider's key
Your app runs on port 8080 instead of 5141?
Update group_vars/all.yml:
backend_port: 8080Ensure your Docker image exposes this port.
Want to use app.example.com and app.example.com/api?
This requires additional Caddy configuration. Update roles/caddy/templates/Caddyfile.j2:
app.example.com {
# Proxy /api to backend
reverse_proxy /api/* localhost:{{ backend_port }}
# Everything else to frontend
reverse_proxy localhost:{{ frontend_port }}
}
Using PostgreSQL instead of MySQL?
You'll need to:
- Modify
roles/application/templates/docker-compose.yml.j2 - Replace MySQL service with PostgreSQL
- Update connection strings in environment variables
Issue: Playbook fails with "Host unreachable"
- Verify server IP is correct in
inventory/hosts.yml - Test SSH manually:
ssh -i ~/.ssh/id_ed25519 user@your-server-ip
Issue: SSL certificate fails
- Ensure DNS is properly configured and propagated
- Check DNS:
dig your-domain.com +short - Wait 10-15 minutes after DNS changes
Issue: Wrong ports exposed
- Check
group_vars/all.ymlport configuration - Verify your Docker images expose the correct ports
- Restart Caddy:
ssh root@server "systemctl restart caddy"
First time user? Make sure to complete the Customization Guide above before proceeding!
Ensure you have customized these files for your environment:
inventory/hosts.yml- Your server IP and SSH settingsgroup_vars/all.yml- Your domains, ports, and GitHub organization- DNS records configured for your domains
cd /path/to/infrastructure/ansibleexport MYSQL_ROOT_PASSWORD="your_secure_root_password"
export MYSQL_PASSWORD="your_database_password"
export GITHUB_TOKEN="your_github_token"
export GITHUB_USERNAME="your_github_username"ssh -i ~/.ssh/id_ed25519 root@188.166.249.118ansible-playbook -i inventory/hosts.yml playbook.ymlAfter successful deployment, verify the services are running:
# SSH into server
ssh root@188.166.249.118
# Check running containers
docker ps
# Check container logs
cd /opt/icu_application
docker-compose logs -f
# Access the applications
# Frontend: https://icu.darwishathibi.com
# Backend: https://api-icu.darwishathibi.comansible/
├── inventory/
│ └── hosts.yml # Server inventory configuration
├── group_vars/
│ └── all.yml # Global variables for all hosts
├── playbook.yml # Main orchestration playbook
└── roles/
├── common/ # System setup and firewall
│ └── tasks/
│ └── main.yml
├── docker/ # Docker installation
│ └── tasks/
│ └── main.yml
├── caddy/ # Reverse proxy configuration
│ ├── tasks/
│ │ └── main.yml
│ └── templates/
│ └── Caddyfile.j2
└── application/ # Application deployment
├── tasks/
│ └── main.yml
└── templates/
└── docker-compose.yml.j2
- inventory/: Defines target servers and connection details
- group_vars/: Variables applied to all hosts (domains, ports, credentials)
- playbook.yml: Main playbook that orchestrates all roles
- roles/: Modular task collections for different infrastructure components
Edit inventory/hosts.yml to configure target server:
all:
hosts:
icu_server:
ansible_host: 188.166.249.118 # CUSTOMIZE: Your server IP address
ansible_user: root # CUSTOMIZE: SSH user (root, ubuntu, etc.)
ansible_ssh_private_key_file: ~/.ssh/id_ed25519 # CUSTOMIZE: Path to SSH key
ansible_python_interpreter: /usr/bin/python3Important: See the Customization Guide for step-by-step instructions on customizing for your server.
Edit group_vars/all.yml to customize deployment:
# Domain Configuration (CUSTOMIZE THESE)
frontend_domain: "icu.darwishathibi.com" # Your frontend domain
backend_domain: "api-icu.darwishathibi.com" # Your backend API domain
# Port Configuration (CUSTOMIZE IF NEEDED)
frontend_port: 5173 # Port your frontend container exposes
backend_port: 5141 # Port your backend container exposes
# Docker Configuration
docker_compose_version: "3.8"
# Database Configuration (values from environment)
mysql_root_password: "{{ lookup('env', 'MYSQL_ROOT_PASSWORD') }}"
mysql_database: "icu_db" # CUSTOMIZE database name
mysql_user: "mandur_gede" # CUSTOMIZE database user
mysql_password: "{{ lookup('env', 'MYSQL_PASSWORD') }}"
# Application Configuration (CUSTOMIZE THESE)
app_directory: "/opt/icu_application" # Where to deploy on server
github_org: "Deta-Technologies" # Your GitHub org/usernameImportant: See the Customization Guide for detailed instructions on customizing for your infrastructure.
Create a .env file or export these variables before running:
# Database credentials
export MYSQL_ROOT_PASSWORD="your_secure_root_password"
export MYSQL_PASSWORD="your_database_password"
# GitHub Container Registry access
export GITHUB_TOKEN="ghp_xxxxxxxxxxxx"
export GITHUB_USERNAME="your_github_username"Deploy complete infrastructure from scratch:
ansible-playbook -i inventory/hosts.yml playbook.ymlPreview changes without applying them:
ansible-playbook -i inventory/hosts.yml playbook.yml --checkExecute only specific roles using tags:
# Only install Docker
ansible-playbook -i inventory/hosts.yml playbook.yml --tags docker
# Only update Caddy configuration
ansible-playbook -i inventory/hosts.yml playbook.yml --tags caddy
# Only deploy application
ansible-playbook -i inventory/hosts.yml playbook.yml --tags applicationGet detailed execution information:
# Verbose mode
ansible-playbook -i inventory/hosts.yml playbook.yml -v
# Very verbose (includes task details)
ansible-playbook -i inventory/hosts.yml playbook.yml -vv
# Debug level (shows everything)
ansible-playbook -i inventory/hosts.yml playbook.yml -vvvTo update just the application (pull new images and restart):
# SSH into server
ssh root@188.166.249.118
# Navigate to application directory
cd /opt/icu_application
# Pull latest images
docker-compose pull
# Restart services
docker-compose up -d
# View logs
docker-compose logs -fIntegrate this Ansible playbook with GitHub Actions for automated deployments.
Create .github/workflows/deploy.yml in your repository:
name: Deploy ICU Application
on:
push:
branches:
- main
workflow_dispatch:
jobs:
build-and-push:
name: Build and Push Docker Images
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push backend image
run: |
docker build -t ghcr.io/deta-technologies/icu-backend:latest ./backend
docker push ghcr.io/deta-technologies/icu-backend:latest
- name: Build and push frontend image
run: |
docker build -t ghcr.io/deta-technologies/icu-frontend:latest ./frontend
docker push ghcr.io/deta-technologies/icu-frontend:latest
deploy:
name: Deploy to Production
needs: build-and-push
runs-on: ubuntu-latest
steps:
- name: Checkout infrastructure repo
uses: actions/checkout@v4
with:
repository: your-org/infrastructure
token: ${{ secrets.GH_PAT }}
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.x'
- name: Install Ansible
run: |
pip install ansible
- name: Configure SSH
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
ssh-keyscan 188.166.249.118 >> ~/.ssh/known_hosts
- name: Run Ansible playbook
env:
MYSQL_ROOT_PASSWORD: ${{ secrets.MYSQL_ROOT_PASSWORD }}
MYSQL_PASSWORD: ${{ secrets.MYSQL_PASSWORD }}
GITHUB_TOKEN: ${{ secrets.GH_PAT }}
GITHUB_USERNAME: ${{ github.actor }}
run: |
cd ansible
ansible-playbook -i inventory/hosts.yml playbook.ymlAdd these secrets to your GitHub repository (Settings → Secrets and variables → Actions):
SSH_PRIVATE_KEY: Your SSH private key for server accessMYSQL_ROOT_PASSWORD: MySQL root passwordMYSQL_PASSWORD: MySQL application user passwordGH_PAT: GitHub Personal Access Token withread:packagesandrepopermissions
For faster deployments after initial setup, create .github/workflows/update.yml:
name: Update Application
on:
workflow_dispatch:
jobs:
update:
name: Pull and Restart Containers
runs-on: ubuntu-latest
steps:
- name: Configure SSH
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
ssh-keyscan 188.166.249.118 >> ~/.ssh/known_hosts
- name: Deploy latest images
run: |
ssh root@188.166.249.118 << 'EOF'
cd /opt/icu_application
docker-compose pull
docker-compose up -d
docker-compose logs -f --tail=50
EOFPurpose: System foundation setup
Tasks:
- Updates all system packages
- Installs essential dependencies (git, curl, python3-pip)
- Configures UFW firewall:
- Allows SSH (22), HTTP (80), HTTPS (443)
- Denies all other incoming traffic
Idempotency: Safe to run multiple times
Location: roles/common/tasks/main.yml
Purpose: Install Docker container runtime
Tasks:
- Adds Docker official GPG key and repository
- Installs Docker CE, Docker Compose plugin, and Buildx
- Configures Docker service to start on boot
- Adds ansible user to docker group
- Installs Docker Compose v2.24.0 standalone
Idempotency: Only installs if not already present
Verification: Displays Docker version after installation
Location: roles/docker/tasks/main.yml
Purpose: Reverse proxy with automatic HTTPS
Tasks:
- Installs Caddy web server from official repository
- Deploys Caddyfile configuration from template
- Configures reverse proxy for frontend and backend
- Enables automatic Let's Encrypt SSL certificates
- Sets up CORS headers for API
Template: roles/caddy/templates/Caddyfile.j2
Features:
- Automatic HTTPS with certificate renewal
- Gzip compression
- HTTP/2 support
- CORS configuration
Handler: Reloads Caddy when configuration changes
Location: roles/caddy/tasks/main.yml
Purpose: Deploy ICU application stack
Tasks:
- Creates application directory at
/opt/icu_application - Creates persistent storage for MySQL data and logs
- Deploys docker-compose.yml from template
- Creates secure .env file with credentials (mode 0600)
- Logs into GitHub Container Registry
- Pulls and starts all containers
Template: roles/application/templates/docker-compose.yml.j2
Services Deployed:
- MySQL 8.0 database
- ASP.NET Core backend API
- SvelteKit frontend application
Location: roles/application/tasks/main.yml
- Never commit credentials: Use environment variables or Ansible Vault
- Use Ansible Vault for secrets (optional but recommended):
# Create encrypted variables file ansible-vault create group_vars/secrets.yml # Run playbook with vault password ansible-playbook -i inventory/hosts.yml playbook.yml --ask-vault-pass
- Use SSH keys, not passwords: Key-based authentication is enforced
- Restrict SSH key permissions:
chmod 600 ~/.ssh/id_ed25519 chmod 644 ~/.ssh/id_ed25519.pub
UFW firewall is configured to:
- Allow only SSH (22), HTTP (80), and HTTPS (443)
- Deny all other incoming traffic
- Allow all outgoing traffic
- Non-root containers: Consider running containers as non-root users
- Regular updates: Keep Docker images updated
- Image scanning: Scan images for vulnerabilities
- .env file permissions: Set to 0600 (owner read/write only)
- Strong passwords: Use complex passwords for MySQL
- Network isolation: MySQL only accessible within Docker network
- Automatic certificates: Caddy handles Let's Encrypt automatically
- Certificate renewal: Automatic, no manual intervention needed
- Force HTTPS: Caddy automatically redirects HTTP to HTTPS
After successful deployment, the following services will be running:
- Container:
icu-mysql - Port: 3306 (internal only)
- Version: MySQL 8.0
- Database:
icu_db - User:
mandur_gede - Data: Persisted in
/opt/icu_application/mysql-data - Health Check: Automated with retry logic
- Container:
icu-backend - Port: 5141 (proxied via Caddy)
- Runtime: ASP.NET Core
- Domain:
https://api-icu.darwishathibi.com - Environment: Production mode
- Logs: Stored in
/opt/icu_application/logs - Dependencies: Waits for healthy MySQL
- Container:
icu-frontend - Port: 5173 (proxied via Caddy)
- Framework: SvelteKit
- Domain:
https://icu.darwishathibi.com - Environment: Production mode
- API Connection:
https://api-icu.darwishathibi.com - Dependencies: Waits for backend
- Service: System service (systemd)
- Ports: 80 (HTTP), 443 (HTTPS)
- Configuration:
/etc/caddy/Caddyfile - SSL Certificates: Automatic via Let's Encrypt
- Features: Gzip compression, CORS, automatic HTTPS
| Service | Internal Port | External Access |
|---|---|---|
| Frontend | 5173 | icu.darwishathibi.com:443 |
| Backend | 5141 | api-icu.darwishathibi.com:443 |
| MySQL | 3306 | Internal only (Docker network) |
| Caddy | 80, 443 | Public |
Persistent data is stored at /opt/icu_application/:
/opt/icu_application/
├── mysql-data/ # MySQL database files (persistent)
├── logs/ # Application logs
├── docker-compose.yml # Service orchestration
└── .env # Environment variables (secure)
All containers are configured with restart policies:
- Containers restart automatically on failure
- Containers start automatically on server reboot
- Docker service starts on system boot
# SSH into server
ssh root@188.166.249.118
# Check all containers
docker ps
# Check specific container logs
docker logs icu-frontend
docker logs icu-backend
docker logs icu-mysql
# Check Caddy status
systemctl status caddyIssue: Containers not starting
# Check docker-compose logs
cd /opt/icu_application
docker-compose logs
# Restart services
docker-compose down
docker-compose up -dIssue: SSL certificate not working
- Verify DNS records point to server IP
- Check Caddy logs:
journalctl -u caddy -f - Ensure ports 80 and 443 are open
Issue: Database connection failed
# Verify MySQL is healthy
docker exec icu-mysql mysql -u root -p -e "SELECT 1;"
# Check MySQL logs
docker logs icu-mysqlMaintained by: Deta Technologies Last Updated: January 2025 Ansible Version: 2.9+