volumes: postgres_data: jenkins_home: gitea_data: traefik_letsencrypt: networks: proxy: # internet-facing network, containers reachable via Traefik driver: bridge name: proxy internal: # no internet access; for DB + backend services only driver: bridge internal: true name: internal socket-proxy: # isolated network just for Traefik <-> Docker API access driver: bridge name: socket-proxy services: # --------------------------------------------------------------------- # Restricts Docker API access so Traefik never touches the raw socket. # Only exposes the read-only "list containers" endpoint it needs to # discover services and their labels. # --------------------------------------------------------------------- socket-proxy: image: tecnativa/docker-socket-proxy:0.3 container_name: socket-proxy restart: unless-stopped environment: CONTAINERS: 1 SERVICES: 0 TASKS: 0 NETWORKS: 0 POST: 0 volumes: - /var/run/docker.sock:/var/run/docker.sock:ro networks: - socket-proxy traefik: image: traefik:v3.6 container_name: traefik restart: unless-stopped command: - "--api.dashboard=true" - "--api.insecure=false" - "--providers.docker=true" - "--providers.docker.endpoint=tcp://socket-proxy:2375" - "--providers.docker.exposedbydefault=false" - "--entrypoints.web.address=:80" - "--entrypoints.websecure.address=:443" - "--certificatesresolvers.le.acme.email=${ACME_EMAIL}" - "--certificatesresolvers.le.acme.storage=/letsencrypt/acme.json" - "--certificatesresolvers.le.acme.httpchallenge=true" - "--certificatesresolvers.le.acme.httpchallenge.entrypoint=web" - "--entrypoints.web.http.redirections.entrypoint.to=websecure" - "--entrypoints.web.http.redirections.entrypoint.scheme=https" - "--log.level=WARN" ports: - "80:80" - "443:443" volumes: - traefik_letsencrypt:/letsencrypt networks: - proxy - socket-proxy labels: - "traefik.enable=true" # Global security headers applied to every router below - "traefik.http.middlewares.sec-headers.headers.stsSeconds=31536000" - "traefik.http.middlewares.sec-headers.headers.stsIncludeSubdomains=true" - "traefik.http.middlewares.sec-headers.headers.forceSTSHeader=true" - "traefik.http.middlewares.sec-headers.headers.frameDeny=true" - "traefik.http.middlewares.sec-headers.headers.contentTypeNosniff=true" - "traefik.http.middlewares.sec-headers.headers.browserXssFilter=true" postgres: image: postgres:16 container_name: postgres restart: unless-stopped environment: POSTGRES_DB: ${POSTGRES_DB} POSTGRES_USER: ${POSTGRES_USER} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} volumes: - postgres_data:/var/lib/postgresql/data networks: - internal # no "proxy" network, no published ports -> unreachable from outside healthcheck: test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"] interval: 30s timeout: 10s retries: 5 apache: image: httpd:2.4 container_name: website restart: unless-stopped volumes: - ./website:/usr/local/apache2/htdocs:ro networks: - proxy labels: - "traefik.enable=true" - "traefik.http.routers.website.rule=Host(`${DOMAIN}`) || Host(`www.${DOMAIN}`)" - "traefik.http.routers.website.entrypoints=websecure" - "traefik.http.routers.website.tls.certresolver=le" - "traefik.http.routers.website.middlewares=sec-headers@docker" - "traefik.http.services.website.loadbalancer.server.port=80" gitea: image: gitea/gitea:1.22 container_name: gitea restart: unless-stopped environment: USER_UID: 1000 USER_GID: 1000 GITEA__server__ROOT_URL: https://git.${DOMAIN}/ volumes: - gitea_data:/data networks: - proxy labels: - "traefik.enable=true" - "traefik.http.routers.gitea.rule=Host(`git.${DOMAIN}`)" - "traefik.http.routers.gitea.entrypoints=websecure" - "traefik.http.routers.gitea.tls.certresolver=le" - "traefik.http.routers.gitea.middlewares=sec-headers@docker" - "traefik.http.services.gitea.loadbalancer.server.port=3000" jenkins: image: jenkins/jenkins:lts-jdk21 container_name: jenkins restart: unless-stopped volumes: - jenkins_home:/var/jenkins_home - /var/run/docker.sock:/var/run/docker.sock # see README: known risk, needed for CI docker builds group_add: - "988" # host docker group GID networks: - proxy labels: - "traefik.enable=true" - "traefik.http.routers.jenkins.rule=Host(`ci.${DOMAIN}`)" - "traefik.http.routers.jenkins.entrypoints=websecure" - "traefik.http.routers.jenkins.tls.certresolver=le" - "traefik.http.routers.jenkins.middlewares=sec-headers@docker" - "traefik.http.services.jenkins.loadbalancer.server.port=8080" api: build: ./api container_name: api restart: unless-stopped networks: - proxy - internal # future: talk to postgres directly, still no internet exposure beyond Traefik labels: - "traefik.enable=true" - "traefik.docker.network=proxy" - "traefik.http.routers.api.rule=Host(`api.${DOMAIN}`)" - "traefik.http.routers.api.entrypoints=websecure" - "traefik.http.routers.api.tls.certresolver=le" - "traefik.http.routers.api.middlewares=sec-headers@docker" - "traefik.http.services.api.loadbalancer.server.port=3000"