# Image pins live HERE, as interpolation defaults, so `git pull` (or # re-downloading this file) delivers the exact version combination this # repository has tested — no .env editing required. Setting the matching # variable in .env overrides the default for anyone who wants to pin # differently; .env itself carries only secrets and deliberate overrides. # CI (Trivy scans, check-pin-freshness, the ephemeral test .env) reads # the pins from these lines — this block is the single source of truth. x-images: traefik: &traefik-image ${TRAEFIK_IMAGE_TAG:-traefik:${TRAEFIK_IMAGE_VERSION:-3.7@sha256:f4bb143d1135a579b630ed491e17ec50f6f7c9c6a30585ff3d385471d2a86f5a}} sftp: &sftp-image ${SFTP_IMAGE_TAG:-atmoz/sftp:${SFTP_IMAGE_VERSION:-debian@sha256:54efe2a6cda236d713042e17a836d970342eea5a7568e4f60800432cec4da205}} networks: sftp-network: external: true traefik-network: external: true volumes: traefik-certificates: services: sftp: image: *sftp-image # Hardening: no privilege escalation via setuid binaries security_opt: - no-new-privileges:true # Resource limits as .env-overridable defaults (e.g. SFTP_MEMORY_LIMIT=2g); # the override survives every git pull. deploy: resources: limits: memory: ${SFTP_MEMORY_LIMIT:-512m} cpus: "${SFTP_CPU_LIMIT:-1.0}" reservations: memory: ${SFTP_MEMORY_RESERVATION:-64m} cpus: "${SFTP_CPU_RESERVATION:-0.25}" # THE USERS GO IN THE ENVIRONMENT, NOT ON THE COMMAND LINE. As command # arguments, user:password:uid sat in the entrypoint's arguments while it # created the users and generated the host keys, where every user on the # host reads them with ps, and in the container's recorded command. The # image reads the same list, space-separated, from SFTP_USERS. environment: SFTP_USERS: >- ${SFTP_USER_1:-user1}:${SFTP_USER_1_PASSWORD:?set in .env - see .env.example for generation command}:${SFTP_USER_1_UID:-1001}:: ${SFTP_USER_2:-user2}:${SFTP_USER_2_PASSWORD:?set in .env - see .env.example for generation command}:${SFTP_USER_2_UID:-1002}:: volumes: - /srv/sftpusers/${SFTP_USER_1:-user1}:/home/${SFTP_USER_1:-user1}/data - /srv/sftpusers/${SFTP_USER_2:-user2}:/home/${SFTP_USER_2:-user2}/data networks: - sftp-network - traefik-network labels: # Enable Traefik for this container - "traefik.enable=true" # Define TCP router rules for SFTP to match all incoming requests (HostSNI) - "traefik.tcp.routers.sftp.rule=HostSNI(`*`)" # Assign the SFTP router to a named Traefik service - "traefik.tcp.routers.sftp.service=sftp" # Use the 'SFTP' (custom) entry point - "traefik.tcp.routers.sftp.entrypoints=ssh" # Define the internal container port for routing to the SFTP service - "traefik.tcp.services.sftp.loadbalancer.server.port=22" # Specify which Docker network Traefik should use for routing - "traefik.docker.network=traefik-network" restart: unless-stopped traefik: image: *traefik-image # Hardening: no privilege escalation via setuid binaries security_opt: - no-new-privileges:true # Drop every Linux capability; add back only what this image's # entrypoint needs (bind :80/:443, chown its data directory, drop to # its service user). cap_drop: - ALL cap_add: - NET_BIND_SERVICE # Resource limits as .env-overridable defaults (e.g. TRAEFIK_MEMORY_LIMIT=2g); # the override survives every git pull. deploy: resources: limits: memory: ${TRAEFIK_MEMORY_LIMIT:-256m} cpus: "${TRAEFIK_CPU_LIMIT:-0.5}" reservations: memory: ${TRAEFIK_MEMORY_RESERVATION:-64m} cpus: "${TRAEFIK_CPU_RESERVATION:-0.1}" command: # Set the log level (DEBUG, INFO, WARN, ERROR) - "--log.level=${TRAEFIK_LOG_LEVEL:-WARN}" # Enable the built-in API and web-based dashboard - "--api.dashboard=true" # Enable the /ping endpoint so we can health-check Traefik - "--ping=true" # Assign the /ping endpoint to a dedicated entry point on port 8082 - "--ping.entrypoint=ping" - "--entrypoints.ping.address=:8082" # Define the primary HTTP entry point on port 80 - "--entrypoints.web.address=:80" # Define the secure (HTTPS) entry point on port 443 - "--entrypoints.websecure.address=:443" # Timeouts on the HTTPS entry point. Unset, they are Traefik's own # defaults; set them in .env to tune (see .env.example). - "--entrypoints.websecure.transport.respondingTimeouts.readTimeout=${TRAEFIK_READ_TIMEOUT:-60s}" - "--entrypoints.websecure.transport.respondingTimeouts.writeTimeout=${TRAEFIK_WRITE_TIMEOUT:-0s}" - "--entrypoints.websecure.transport.respondingTimeouts.idleTimeout=${TRAEFIK_IDLE_TIMEOUT:-180s}" # HTTP -> HTTPS redirect at entrypoint level - "--entrypoints.web.http.redirections.entrypoint.to=websecure" - "--entrypoints.web.http.redirections.entrypoint.scheme=https" # Define the SFTP entry point using an environment variable for the port - "--entryPoints.ssh.address=:${SFTP_PORT:-2222}" # Enable the Docker provider to detect containers and their labels - "--providers.docker=true" # Point Traefik to the Docker socket - "--providers.docker.endpoint=unix:///var/run/docker.sock" # Prevent automatic exposure of all containers; only expose containers # with "traefik.enable=true" - "--providers.docker.exposedbydefault=false" # Use ACME (Let's Encrypt) to generate/renew certificates via TLS challenge - "--certificatesresolvers.letsencrypt.acme.tlschallenge=true" # The email address used by Let's Encrypt for renewal notices - "--certificatesresolvers.letsencrypt.acme.email=${TRAEFIK_ACME_EMAIL:?set in .env - see .env.example}" # The file where ACME certificates are stored inside the container - "--certificatesresolvers.letsencrypt.acme.storage=/etc/traefik/acme/acme.json" # Enable Prometheus metrics - "--metrics.prometheus=true" # Configure Prometheus histogram buckets - "--metrics.prometheus.buckets=0.1,0.3,1.2,5.0" # Check for newer Traefik versions and optionally log that info - "--global.checknewversion=true" # Disable sending anonymous usage data to the Traefik maintainers - "--global.sendanonymoususage=false" volumes: - /var/run/docker.sock:/var/run/docker.sock:ro - traefik-certificates:/etc/traefik/acme networks: - traefik-network ports: - "${SFTP_PORT:-2222}:${SFTP_PORT:-2222}" - "80:80" - "443:443" healthcheck: test: ["CMD", "wget", "http://localhost:8082/ping","--spider"] interval: 10s timeout: 5s retries: 3 start_period: 5s labels: # Enable Traefik for this container - "traefik.enable=true" # A router to expose the Traefik dashboard - "traefik.http.routers.dashboard.rule=Host(`${TRAEFIK_HOSTNAME:?set in .env - see .env.example}`)" - "traefik.http.routers.dashboard.entrypoints=websecure" - "traefik.http.routers.dashboard.tls=true" - "traefik.http.routers.dashboard.tls.certresolver=letsencrypt" - "traefik.http.routers.dashboard.service=api@internal" # Basic Authentication for the Traefik dashboard - "traefik.http.routers.dashboard.middlewares=authtraefik" - "traefik.http.middlewares.authtraefik.basicauth.users=${TRAEFIK_BASIC_AUTH:?set in .env - see .env.example for htpasswd generation}" # Specify the internal server port to the dashboard service - "traefik.http.services.dashboard.loadbalancer.server.port=8080" # Pass the original Host header to the backend - "traefik.http.services.dashboard.loadbalancer.passhostheader=true" restart: unless-stopped