diff --git a/caddy-fastapi/.gitignore b/caddy-fastapi/.gitignore new file mode 100644 index 0000000..a8f296d --- /dev/null +++ b/caddy-fastapi/.gitignore @@ -0,0 +1,5 @@ +__pycache__ + +# container volume files +data/* +!data/.gitkeep diff --git a/caddy-fastapi/README.md b/caddy-fastapi/README.md new file mode 100644 index 0000000..f62b05e --- /dev/null +++ b/caddy-fastapi/README.md @@ -0,0 +1,147 @@ +# Compose sample application + +## Caddy/FastAPI application ⛳ + +Deploy [Caddy](https://caddyserver.com/) + [FastAPI](https://fastapi.tiangolo.com/) with docker-compose + +Project structure: + +```text +├── docker-compose.yaml +├── Dockerfile +├── requirements.txt +├── src + ├── caddy + ├── Caddyfile + ├── Dockerfile + ├── start.sh + ├── caddy + ├── Dockerfile + ├── main.py + ├── requirements.txt +``` + +[_docker-compose.yaml_](docker-compose.yaml) + +```yaml +services: + fastapi: + container_name: fastapi + restart: unless-stopped + build: + context: ./src/fastapi + dockerfile: ./Dockerfile + ports: + - 8000:8000 + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8000/health"] + interval: 5m + timeout: 5s + retries: 3 + start_period: 15s + + caddy: + container_name: caddy + restart: unless-stopped + build: + context: ./src/caddy + dockerfile: ./Dockerfile + ports: + - 80:80 + - 443:443 + volumes: + - ./data/caddy_data:/data + - ./data/caddy_config:/config + depends_on: + - fastapi + environment: + PROXY_BACKEND: fastapi + PROXY_PORT: 8000 + DOMAIN: ${DOMAIN} + +volumes: + caddy_data: + caddy_config: +``` + +## Deploy with docker-compose + +```bash +docker-compose up --build +``` + +> Note: You will see `WARNING: The DOMAIN variable is not set. Defaulting to a blank string.` and that is expected - See the extra info section below for more details + +## Expected result + +Listing containers must show one container running and the port mapping as below: + +```console +$ docker ps +CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES +52d5fbe3dc5d caddy-fastapi_caddy "sh /app/start.sh" 12 seconds ago Up 1 second 0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp, 2019/tcp caddy +09677bb1297e caddy-fastapi_fastapi "python -m uvicorn m…" 13 seconds ago Up 1 second (health: starting) 0.0.0.0:8000->8000/tcp +``` + +After the application starts, navigate to [localhost](https://localhost:443/) in your web browser and you should see the following json response: + +```json +{"Hello":"World"} +``` + +Stop and remove the containers + +```console +$ docker-compose down +WARNING: The DOMAIN variable is not set. Defaulting to a blank string. +Stopping caddy ... done +Stopping fastapi ... done +Removing caddy ... done +Removing fastapi ... done +Removing network caddy-fastapi_default +``` + +## Additional Information 📚 + +This section contains additional information about the docker-compose sample application + +### TLS Certificate 🔐 + +Caddy automatically provisions TLS certificates for you. In order to make use of this awesome feature, do the following: + +1. Ensure your server has ports `80` and `443` open +1. Have a DNS record pointed to your server for the domain you wish to obtain a certificate for (e.g. `app.example.org` -> ``) +1. Export the env var for the domain you wish to use: + + ```bash + export DOMAIN=app.example.org + ``` + +1. Start the docker-compose stack: + + ```bash + docker-compose up --build + ``` + +1. Navigate to your domain and enjoy your easy TLS setup with Caddy! -> [https://app.example.org](https://app.example.orgg) + +### Extra Extra Info 📚 + +Here is some extra info about the setup + +#### Volumes 🛢️ + +The docker-compose file creates two volumes: + +- `./data/caddy_data:/data` +- `./data/caddy_config:/config` + +The config volume is used to mount Caddy configuration + +The data volume is used to store certificate information. This is really important so that you are not re-requesting TLS certs each time you start your container. Doing so can cause you to hit Let's Encrypt rate limits that will prevent you from provisioning certificates. + +### Environment Variables 📝 + +If you run the stack without the `DOMAIN` variable set in your environment, the stack will default to using `localhost`. This is ideal for testing out the stack locally. + +If you set the `DOMAIN` variable, Caddy will attempt to provision a certificate for that domain. In order to do so, you will need DNS records pointed to that domain and you will need need traffic to access your server via port `80` and `443`. diff --git a/caddy-fastapi/data/.gitkeep b/caddy-fastapi/data/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/caddy-fastapi/docker-compose.yml b/caddy-fastapi/docker-compose.yml new file mode 100644 index 0000000..1af5461 --- /dev/null +++ b/caddy-fastapi/docker-compose.yml @@ -0,0 +1,42 @@ +# To build the entire stack run 'make run' + +version: '3.7' + +services: + fastapi: + container_name: fastapi + restart: unless-stopped + build: + context: ./src/fastapi + dockerfile: ./Dockerfile + ports: + - 8000:8000 + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8000/health"] + interval: 5m + timeout: 5s + retries: 3 + start_period: 15s + + caddy: + container_name: caddy + restart: unless-stopped + build: + context: ./src/caddy + dockerfile: ./Dockerfile + ports: + - 80:80 + - 443:443 + volumes: + - ./data/caddy_data:/data + - ./data/caddy_config:/config + depends_on: + - fastapi + environment: + PROXY_BACKEND: fastapi + PROXY_PORT: 8000 + DOMAIN: ${DOMAIN} + +volumes: + caddy_data: + caddy_config: diff --git a/caddy-fastapi/src/caddy/Caddyfile b/caddy-fastapi/src/caddy/Caddyfile new file mode 100644 index 0000000..b64fa18 --- /dev/null +++ b/caddy-fastapi/src/caddy/Caddyfile @@ -0,0 +1,5 @@ +{$DOMAIN} { + reverse_proxy {$PROXY_BACKEND}:{$PROXY_PORT} { + header_down Strict-Transport-Security max-age=31536000; + } +} \ No newline at end of file diff --git a/caddy-fastapi/src/caddy/Dockerfile b/caddy-fastapi/src/caddy/Dockerfile new file mode 100644 index 0000000..fef6efa --- /dev/null +++ b/caddy-fastapi/src/caddy/Dockerfile @@ -0,0 +1,8 @@ +FROM caddy/caddy:2.4.6-alpine + +RUN mkdir /app +COPY start.sh /app/start.sh + +COPY Caddyfile /etc/caddy/Caddyfile + +CMD ["sh", "/app/start.sh"] diff --git a/caddy-fastapi/src/caddy/start.sh b/caddy-fastapi/src/caddy/start.sh new file mode 100644 index 0000000..6715280 --- /dev/null +++ b/caddy-fastapi/src/caddy/start.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +set -e + +if [ -z "$DOMAIN" ] +then + # If DOMAIN is blank, set to localhost + # Note: in prod, domain will be the actual domain + export DOMAIN="localhost" +fi + +caddy run --config /etc/caddy/Caddyfile --adapter caddyfile diff --git a/caddy-fastapi/src/fastapi/Dockerfile b/caddy-fastapi/src/fastapi/Dockerfile new file mode 100644 index 0000000..d574cec --- /dev/null +++ b/caddy-fastapi/src/fastapi/Dockerfile @@ -0,0 +1,27 @@ +FROM python:alpine3.15 + +WORKDIR /app + +# Install curl for healthchecks +RUN apk add curl + +# Setup a nonroot user for security +RUN adduser -D nonroot +USER nonroot + +# Upgrade pip +RUN pip install --upgrade pip + +# Install dependencies +COPY requirements.txt . +RUN pip install --user --no-cache-dir --upgrade -r requirements.txt + +# Copy the app +COPY main.py /app/main.py + +# Expose the app's port +EXPOSE 8000 + +# Run the FastAPI server +ENTRYPOINT ["python", "-m"] +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/caddy-fastapi/src/fastapi/main.py b/caddy-fastapi/src/fastapi/main.py new file mode 100644 index 0000000..4fb7a4b --- /dev/null +++ b/caddy-fastapi/src/fastapi/main.py @@ -0,0 +1,14 @@ +from typing import Optional + +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/") +def read_root(): + return {"Hello": "World"} + +@app.get("/health") +def read_root(): + return "OK" diff --git a/caddy-fastapi/src/fastapi/requirements.txt b/caddy-fastapi/src/fastapi/requirements.txt new file mode 100644 index 0000000..6edd71c --- /dev/null +++ b/caddy-fastapi/src/fastapi/requirements.txt @@ -0,0 +1,2 @@ +fastapi==0.75.0 +uvicorn==0.17.6