Skip to content

Commit a55ad66

Browse files
author
Tom Softreck
committed
caddy multiservice with caddy embedded on docker
1 parent 4c9f03c commit a55ad66

File tree

14 files changed

+551
-409
lines changed

14 files changed

+551
-409
lines changed

caddy/Caddyfile

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,20 @@
11
api.naszed.de {
22
reverse_proxy myapi:8080
33
tls {
4-
dns cloudflare {env.CLOUDFLARE_API_TOKEN}
4+
dns cloudflare {env.CLOUDFLARE_API_TOKEN} {
5+
email {env.CLOUDFLARE_EMAIL}
6+
}
7+
}
8+
log {
9+
output file /var/log/caddy/access.log {
10+
roll_size 10MB
11+
roll_keep 10
12+
}
13+
}
14+
errors {
15+
output file /var/log/caddy/error.log {
16+
roll_size 10MB
17+
roll_keep 10
18+
}
519
}
620
}

caddy/ansible-requirements.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
collections:
3+
- name: community.docker
4+
version: 3.8.1
5+
- name: ansible.posix
6+
version: 1.5.4
7+
- name: community.general
8+
version: 7.5.0

caddy/ansible_tests.yml

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
---
2+
- name: Test Caddy and FastAPI Services
3+
hosts: localhost
4+
connection: local
5+
gather_facts: no
6+
7+
vars:
8+
caddy_domain: "{{ lookup('env', 'DOMAIN') | default('localhost', true) }}"
9+
validate_certs: "{{ 'no' if 'localhost' in caddy_domain else 'yes' }}"
10+
11+
tasks:
12+
- name: Check if Docker is running
13+
ansible.builtin.command: docker info
14+
register: docker_info
15+
changed_when: false
16+
ignore_errors: yes
17+
18+
- name: Fail if Docker is not running
19+
ansible.builtin.fail:
20+
msg: "Docker is not running. Please start Docker and try again."
21+
when: docker_info.rc != 0
22+
23+
- name: Check container status using shell
24+
ansible.builtin.shell: |
25+
for container in caddy myapi; do
26+
if docker ps --format '{{ '{{.Names}}' }}' | grep -q "^$container$"; then
27+
echo "$container:running"
28+
else
29+
echo "$container:stopped"
30+
fi
31+
done
32+
register: container_status
33+
changed_when: false
34+
35+
- name: Display container status
36+
ansible.builtin.debug:
37+
msg: "Container {{ item.split(':')[0] }} is {{ 'running' if item.endswith('running') else 'not running' }}"
38+
loop: "{{ container_status.stdout_lines }}"
39+
40+
- name: Set container status fact
41+
ansible.builtin.set_fact:
42+
all_containers_running: "{{ container_status.stdout | regex_replace('\\n', '') == 'caddy:runningmyapi:running' }}"
43+
44+
- name: Fail if containers are not running
45+
ansible.builtin.fail:
46+
msg: "Some containers are not running. Please check with 'docker-compose ps'"
47+
when: not all_containers_running
48+
49+
- name: Ensure virtualenv is available
50+
ansible.builtin.pip:
51+
name: virtualenv
52+
state: present
53+
executable: "{{ ansible_python_interpreter | default('python3') }}"
54+
delegate_to: localhost
55+
run_once: true
56+
become: false
57+
58+
- name: Create a virtual environment
59+
ansible.builtin.command: "{{ ansible_python_interpreter | default('python3') }} -m venv {{ playbook_dir }}/.venv"
60+
args:
61+
creates: "{{ playbook_dir }}/.venv/bin/activate"
62+
delegate_to: localhost
63+
run_once: true
64+
become: false
65+
66+
- name: Install required Python packages
67+
ansible.builtin.pip:
68+
name:
69+
- requests
70+
- urllib3
71+
state: present
72+
virtualenv: "{{ playbook_dir }}/.venv"
73+
virtualenv_python: "{{ ansible_python_interpreter | default('python3') }}"
74+
delegate_to: localhost
75+
become: false
76+
77+
- name: Test Caddy HTTPS endpoint
78+
ansible.builtin.uri:
79+
url: "https://{{ caddy_domain }}"
80+
method: GET
81+
validate_certs: "{{ validate_certs }}"
82+
status_code: 200
83+
timeout: 10
84+
register: caddy_test
85+
retries: 3
86+
delay: 5
87+
until: caddy_test is defined and caddy_test.status == 200
88+
ignore_errors: true
89+
90+
- name: Test FastAPI endpoint
91+
ansible.builtin.uri:
92+
url: "https://{{ caddy_domain }}/docs"
93+
method: GET
94+
validate_certs: "{{ validate_certs }}"
95+
status_code: 200
96+
timeout: 10
97+
register: fastapi_test
98+
retries: 3
99+
delay: 5
100+
until: fastapi_test is defined and fastapi_test.status == 200
101+
ignore_errors: true
102+
103+
- name: Display test results
104+
ansible.builtin.debug:
105+
msg: |
106+
=== Test Results ===
107+
Caddy HTTPS: {{ '✅ PASS' if caddy_test.status == 200 else '❌ FAIL' }}
108+
FastAPI Docs: {{ '✅ PASS' if fastapi_test.status == 200 else '❌ FAIL' }}
109+
===================
110+
111+
- name: Get container logs if tests failed
112+
ansible.builtin.shell: docker logs --tail 20 {{ item }}
113+
register: container_logs
114+
loop:
115+
- caddy
116+
- myapi
117+
when:
118+
- caddy_test is defined
119+
- fastapi_test is defined
120+
- (caddy_test.status | default(0) != 200) or (fastapi_test.status | default(0) != 200)
121+
changed_when: false
122+
ignore_errors: true
123+
124+
- name: Show container logs if tests failed
125+
ansible.builtin.debug:
126+
msg: |
127+
=== Container Logs ===
128+
{% for log in container_logs.results %}
129+
--- {{ log.item }} ---
130+
{{ log.stdout | default('No logs available') }}
131+
132+
{% endfor %}
133+
when: container_logs is defined and container_logs.results is defined
134+
135+
- name: Fail if tests failed
136+
ansible.builtin.fail:
137+
msg: |
138+
Some tests failed. Check the logs above for details.
139+
Caddy status: {{ caddy_test.status | default('Not tested') }}
140+
FastAPI status: {{ fastapi_test.status | default('Not tested') }}
141+
when:
142+
- caddy_test is defined
143+
- fastapi_test is defined
144+
- (caddy_test.status | default(0) != 200) or (fastapi_test.status | default(0) != 200)

caddy/docker-compose.yml

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,27 @@
11
services:
22
caddy:
3-
image: caddy:2.8.0
3+
image: lucaslorentz/caddy-docker-proxy:2.8.0-alpine
44
container_name: caddy
55
ports:
66
- "80:80"
77
- "443:443"
88
volumes:
9-
- ./Caddyfile:/etc/caddy/Caddyfile
9+
- /var/run/docker.sock:/var/run/docker.sock:ro
10+
- ./Caddyfile:/etc/caddy/Caddyfile:ro
1011
- caddy_data:/data
1112
- caddy_config:/config
13+
- caddy_logs:/var/log/caddy
1214
environment:
1315
- CLOUDFLARE_API_TOKEN=${CLOUDFLARE_API_TOKEN}
16+
- CLOUDFLARE_EMAIL=${CLOUDFLARE_EMAIL}
1417
networks:
1518
- web
1619
restart: unless-stopped
20+
healthcheck:
21+
test: ["CMD", "wget", "--spider", "-q", "http://localhost:80/"]
22+
interval: 10s
23+
timeout: 5s
24+
retries: 3
1725

1826
myapi:
1927
build: .
@@ -30,4 +38,5 @@ networks:
3038

3139
volumes:
3240
caddy_data:
33-
caddy_config:
41+
caddy_config:
42+
caddy_logs:

caddy/install.sh

Lines changed: 87 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,43 @@
11
#!/bin/bash
22

33
set -e
4-
# from .env import all variables
5-
source .env
6-
7-
# Function to run tests
8-
run_tests() {
9-
echo "🚀 Running tests..."
10-
if [ -f "tests/run_tests.sh" ]; then
11-
cd tests
12-
./run_tests.sh
13-
cd ..
14-
else
15-
echo "⚠️ Test script not found. Skipping tests."
16-
fi
4+
5+
# Load environment variables
6+
if [ -f ".env" ]; then
7+
source .env
8+
else
9+
echo "❌ Error: .env file not found"
10+
exit 1
11+
fi
12+
13+
# Default values
14+
DOMAIN=${DOMAIN:-localhost}
15+
APP_NAME=${APP_NAME:-app}
16+
17+
# Function to check if a container is running
18+
container_is_running() {
19+
docker ps --filter "name=$1" --format '{{.Names}}' | grep -q "^$1$"
20+
}
21+
22+
# Function to wait for a container to be healthy
23+
wait_for_container() {
24+
local container=$1
25+
local max_attempts=30
26+
local wait_seconds=5
27+
28+
echo "⏳ Waiting for $container to be ready..."
29+
30+
for ((i=1; i<=max_attempts; i++)); do
31+
if container_is_running "$container"; then
32+
echo "$container is running"
33+
return 0
34+
fi
35+
echo "$container not ready yet (attempt $i/$max_attempts)..."
36+
sleep $wait_seconds
37+
done
38+
39+
echo "❌ Timeout waiting for $container to start"
40+
return 1
1741
}
1842

1943
echo "📁 Creating project structure..."
@@ -22,7 +46,8 @@ cd $APP_NAME
2246

2347
# Create FastAPI app if it doesn't exist
2448
if [ ! -f "app/main.py" ]; then
25-
echo "from fastapi import FastAPI\n\napp = FastAPI()\n\[email protected](\"/\")\nasync def read_root():\n return {\"message\": \"Hello, World!\"}" > app/main.py
49+
mkdir -p app
50+
echo "from fastapi import FastAPI\nfrom fastapi.middleware.cors import CORSMiddleware\n\napp = FastAPI()\n\n# Enable CORS\napp.add_middleware(\n CORSMiddleware,\n allow_origins=[\"*\"],\n allow_credentials=True,\n allow_methods=[\"*\"],\n allow_headers=[\"*\"],\n)\n\[email protected](\"/\")\nasync def read_root():\n return {\"message\": \"Hello, World!\"}\n\[email protected](\"/health\")\nasync def health_check():\n return {\"status\": \"healthy\"}\n" > app/main.py
2651
fi
2752

2853
# Create requirements.txt if it doesn't exist
@@ -32,64 +57,69 @@ fi
3257

3358
# Create Dockerfile if it doesn't exist
3459
if [ ! -f "Dockerfile" ]; then
35-
echo "FROM python:3.9-slim\n\nWORKDIR /app\n\nCOPY requirements.txt .\nRUN pip install --no-cache-dir -r requirements.txt\n\nCOPY . .\n\nCMD [\"uvicorn\", \"app.main:app\", \"--host\", \"0.0.0.0\", \"--port\", \"8080\"]" > Dockerfile
60+
echo "FROM python:3.9-slim\n\nWORKDIR /app\n\n# Install system dependencies\nRUN apt-get update && apt-get install -y --no-install-recommends \\\n build-essential \\\n && rm -rf /var/lib/apt/lists/*\n\n# Install Python dependencies\nCOPY requirements.txt .\nRUN pip install --no-cache-dir -r requirements.txt\n\n# Copy application code\nCOPY . .\n\n# Expose port\nEXPOSE 8080\n\n# Run the application\nCMD [\"uvicorn\", \"app.main:app\", \"--host\", \"0.0.0.0\", \"--port\", \"8080\"]" > Dockerfile
3661
fi
3762

3863
# Create docker-compose.yml if it doesn't exist
3964
if [ ! -f "../docker-compose.yml" ]; then
40-
echo "services:
41-
caddy:
42-
image: caddy:2.8.0
43-
container_name: caddy
44-
ports:
45-
- \"80:80\"
46-
- \"443:443\"
47-
volumes:
48-
- ./Caddyfile:/etc/caddy/Caddyfile
49-
- caddy_data:/data
50-
- caddy_config:/config
51-
environment:
52-
- CLOUDFLARE_API_TOKEN=\${CF_API_TOKEN}
53-
networks:
54-
- web
55-
restart: unless-stopped
56-
57-
myapi:
58-
build: .
59-
container_name: myapi
60-
expose:
61-
- \"8080\"
62-
networks:
63-
- web
64-
command: uvicorn app.main:app --host 0.0.0.0 --port 8080 --reload
65-
66-
networks:
67-
web:
68-
external: true
69-
70-
volumes:
71-
caddy_data:
72-
caddy_config:" > ../docker-compose.yml
65+
echo "version: '3.8'\n\nservices:\n caddy:\n image: caddy:2.8.0\n container_name: caddy\n ports:\n - \"80:80\"\n - \"443:443\"\n volumes:\n - ./Caddyfile:/etc/caddy/Caddyfile:ro\n - caddy_data:/data\n - caddy_config:/config\n environment:\n - CLOUDFLARE_API_TOKEN=\${CF_API_TOKEN}\n networks:\n - web\n restart: unless-stopped\n healthcheck:\n test: [\"CMD\", \"curl\", \"-f\", \"http://localhost:80/\"]\n interval: 10s\n timeout: 5s\n retries: 3\n\n myapi:\n build:\n context: .\n dockerfile: Dockerfile\n container_name: myapi\n expose:\n - \"8080\"\n networks:\n - web\n restart: unless-stopped\n healthcheck:\n test: [\"CMD\", \"curl\", \"-f\", \"http://localhost:8080/health\"]\n interval: 10s\n timeout: 5s\n retries: 3\n\nnetworks:\n web:\n driver: bridge\n\nvolumes:\n caddy_data:\n caddy_config:\n" > ../docker-compose.yml
7366
fi
7467

7568
# Create Caddyfile if it doesn't exist
7669
if [ ! -f "../Caddyfile" ]; then
77-
echo "${DOMAIN} {
78-
reverse_proxy myapi:8080
79-
tls {
80-
dns cloudflare {env.CLOUDFLARE_API_TOKEN}
81-
}" > ../Caddyfile
70+
echo "${DOMAIN} {\n # Enable the admin API (optional, for debugging)\n # admin\n \n # Logging\n log {\n output file /var/log/caddy/access.log\n format json\n }\n \n # Handle API requests\n handle_path /api/* {\n reverse_proxy myapi:8080\n }\n \n # Handle all other requests\n handle {\n reverse_proxy myapi:8080\n }\n \n # Enable TLS with Cloudflare DNS challenge\n tls {\n dns cloudflare {env.CLOUDFLARE_API_TOKEN}\n }\n}" > ../Caddyfile
71+
fi
72+
73+
# Create logs directory
74+
mkdir -p ../logs
75+
76+
# Create web network if it doesn't exist
77+
if ! docker network inspect web >/dev/null 2>&1; then
78+
echo "🌐 Creating Docker network 'web'..."
79+
docker network create web
8280
fi
8381

8482
echo "🚀 Starting services..."
8583
cd ..
84+
85+
echo "🔧 Building and starting containers..."
8686
docker-compose up -d --build
8787

88-
echo "⏳ Waiting for services to be ready..."
89-
sleep 10
88+
# Wait for containers to be ready
89+
wait_for_container "caddy"
90+
wait_for_container "myapi"
91+
92+
echo "✅ Services started successfully!"
9093

9194
# Run tests
92-
run_tests
95+
echo -e "\n🚀 Running tests..."
96+
97+
# Run basic tests
98+
if [ -f "./run_tests.sh" ]; then
99+
chmod +x ./run_tests.sh
100+
if ! ./run_tests.sh; then
101+
echo -e "\n${YELLOW}Basic tests failed. Check the output above for details.${NC}"
102+
fi
103+
else
104+
echo -e "\n${YELLOW}⚠️ Basic test script not found. Skipping basic tests.${NC}"
105+
fi
93106

94-
echo "✅ Setup completed successfully!"
107+
# Run Ansible tests
108+
if [ -f "./run_ansible_tests.sh" ]; then
109+
echo -e "\n🔍 Running Ansible tests..."
110+
chmod +x ./run_ansible_tests.sh
111+
if ./run_ansible_tests.sh; then
112+
echo -e "\n${GREEN}✅ Ansible tests completed successfully!${NC}"
113+
else
114+
echo -e "\n${YELLOW}⚠️ Ansible tests completed with some failures.${NC}"
115+
fi
116+
else
117+
echo -e "\n${YELLOW}⚠️ Ansible test script not found. Skipping Ansible tests.${NC}"
118+
fi
119+
120+
echo "\n✅ Setup completed successfully!"
95121
echo "🌐 Your FastAPI app should be available at: https://${DOMAIN}"
122+
echo "📝 API documentation: https://${DOMAIN}/docs"
123+
124+
echo "\n📋 Container status:"
125+
docker ps --filter "name=caddy|myapi" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"

0 commit comments

Comments
 (0)