# Production stack: security.preset "publicRead" equivalent — reads are # open (apt clients need no credentials), the mutating API is behind Basic # Auth. aptly itself is never published to the host; only nginx is. See # docs/security.md for how to move to the other three presets (fully open, # fully authenticated, or read-only), and .env.example for every variable # used below. # # First run: # cp .env.example .env && edit it # cp config/users.example config/users && edit it (at least change the password) # docker compose up -d # docker compose logs -f aptly-init # check for GPG warnings services: aptly-init: image: git.morlana.online/f.weber/aptly:${APTLY_IMAGE_TAG:-latest} entrypoint: ["/usr/local/bin/aptly-init"] environment: APTLY_GPG_ENABLED: "${APTLY_GPG_ENABLED:-true}" # Fixed in-container paths — put your key material at the host paths # below (an empty/missing directory is fine: aptly-init then warns and # publishes unsigned instead of failing to start). APTLY_GPG_PRIVATE_KEY_FILE: "/etc/aptly-secrets/gpg/private.asc" APTLY_GPG_PASSPHRASE_FILE: "/etc/aptly-secrets/gpg/passphrase" APTLY_USERS_FILE: "/etc/aptly-secrets/users" APTLY_INTERNAL_USER: "aptly-internal" APTLY_INTERNAL_PASSWORD: "${APTLY_INTERNAL_PASSWORD:?set APTLY_INTERNAL_PASSWORD in .env}" volumes: - ./config/aptly.yaml:/etc/aptly-src/aptly.yaml:ro - ./config/users:/etc/aptly-secrets/users:ro - ${APTLY_GPG_DIR:-./config/gpg}:/etc/aptly-secrets/gpg:ro - aptly_run:/run/aptly restart: "no" aptly: image: git.morlana.online/f.weber/aptly:${APTLY_IMAGE_TAG:-latest} depends_on: aptly-init: condition: service_completed_successfully environment: APTLY_API_LISTEN: "0.0.0.0:8080" volumes: - aptly_data:/var/lib/aptly - aptly_run:/run/aptly read_only: true tmpfs: - /tmp healthcheck: test: ["CMD", "curl", "-fsS", "http://127.0.0.1:8080/api/healthy"] interval: 10s timeout: 5s retries: 20 start_period: 10s restart: unless-stopped deploy: resources: limits: memory: 1g logging: driver: json-file options: max-size: "10m" max-file: "3" nginx: image: nginxinc/nginx-unprivileged:1-alpine depends_on: aptly: condition: service_healthy volumes: - aptly_data:/var/lib/aptly:ro - aptly_run:/run/aptly:ro - ./config/nginx.prod.conf:/etc/nginx/conf.d/default.conf:ro ports: - "${APTLY_PUBLISH_PORT:-8080}:8080" read_only: true tmpfs: - /tmp - /var/cache/nginx - /run healthcheck: test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8080/healthz"] interval: 10s timeout: 5s retries: 20 restart: unless-stopped deploy: resources: limits: memory: 256m logging: driver: json-file options: max-size: "10m" max-file: "3" # --- TLS via an external reverse proxy (recommended) --- # Put Traefik/Caddy/whatever you already run in front of this service # instead of terminating TLS here. Traefik label example: # labels: # - "traefik.enable=true" # - "traefik.http.routers.aptly.rule=Host(`apt.example.com`)" # - "traefik.http.routers.aptly.tls.certresolver=letsencrypt" reconcile: image: git.morlana.online/f.weber/aptly:${APTLY_IMAGE_TAG:-latest} depends_on: aptly: condition: service_healthy entrypoint: ["/usr/local/bin/aptly-reconcile"] environment: APTLY_URL: "http://aptly:8080" APTLY_STATE_FILE: "/state.yaml" APTLY_FAIL_ON_ERROR: "${APTLY_RECONCILE_FAIL_ON_ERROR:-false}" volumes: - ./config/state.yaml:/state.yaml:ro restart: "no" # Run on demand: docker compose --profile backup run --rm backup backup: image: git.morlana.online/f.weber/aptly:${APTLY_IMAGE_TAG:-latest} profiles: ["backup"] entrypoint: ["/bin/sh", "-c"] command: - > set -eu; ts=$$(date -u +%Y%m%dT%H%M%SZ); tar -C /var/lib/aptly --exclude=.gnupg -c . | zstd -q -o "/backup/aptly-$${ts}.tar.zst"; echo "wrote /backup/aptly-$${ts}.tar.zst" volumes: - aptly_data:/var/lib/aptly:ro - ${APTLY_BACKUP_DIR:-./backup}:/backup volumes: aptly_data: aptly_run: