{"id":20333948,"url":"https://github.com/braedon/bottler","last_synced_at":"2026-05-06T00:07:22.743Z","repository":{"id":146707655,"uuid":"292040784","full_name":"braedon/bottler","owner":"braedon","description":"Bottle up static sites in a Docker container, with strict security defaults and Kubernetes-ready features.","archived":false,"fork":false,"pushed_at":"2021-04-29T05:13:56.000Z","size":44,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-06-01T15:44:46.415Z","etag":null,"topics":["bottlepy","docker","kubernetes","static-site"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/braedon.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-09-01T15:48:36.000Z","updated_at":"2021-04-29T05:13:58.000Z","dependencies_parsed_at":null,"dependency_job_id":"765c548d-8a70-4d7c-b71a-f83bc181fd7b","html_url":"https://github.com/braedon/bottler","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/braedon/bottler","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/braedon%2Fbottler","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/braedon%2Fbottler/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/braedon%2Fbottler/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/braedon%2Fbottler/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/braedon","download_url":"https://codeload.github.com/braedon/bottler/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/braedon%2Fbottler/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32672689,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-05T11:29:49.557Z","status":"ssl_error","status_checked_at":"2026-05-05T11:29:48.587Z","response_time":54,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["bottlepy","docker","kubernetes","static-site"],"created_at":"2024-11-14T20:35:24.081Z","updated_at":"2026-05-06T00:07:22.729Z","avatar_url":"https://github.com/braedon.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"Bottler\n====\nWrap static sites in a simple Docker container for deployment on Kubernetes (or similar).\n\n[Source Code](https://github.com/braedon/bottler) | [Docker Image](https://hub.docker.com/r/braedon/bottler)\n\n# How?\n\nBottler serves your site with the [Bottle micro web-framework](https://bottlepy.org/) on a [gevent WSGI server](https://www.gevent.org/).\n\n# Why?\n\nWhy not just use a standard webserver like Nginx?\n\nBottler provides strict [security headers](https://securityheaders.com/) out of the box, which you can relax based on the needs of your site.\n\nIt's also designed to be simple to deploy on container services.\n\n* JSON structured request logging (enable with the `--json` option),\n* Graceful shutdown on `SIGINT` and `SIGTERM`,\n* `/-/live` and `/-/ready` endpoints for liveness and readiness probes.\n\n**:warning: Here Be Dragons**\n\nTake note of the default values for the [Strict-Transport-Security](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security) and [Expect-CT](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Expect-CT) headers in the [site config file](#site-config-file) before deploying. These headers can cause long term issues if not set correctly for your site.\n\n# Quickstart\n\nDocker images for released versions can be found on Docker Hub (`latest` is available):\n```bash\n\u003e sudo docker pull braedon/bottler\n```\n\nMount your site's static files at `/site/static` and map container port `8080` to a port on the host:\n```bash\n\u003e sudo docker run --rm \\\n    -v \u003cpath to static files\u003e:/site/static \\\n    -p \u003chost port\u003e:8080 \\\n    braedon/bottler\n```\n\nA config file can also be mounted in:\n```bash\n\u003e sudo docker run --rm \\\n    -v \u003cpath to static files\u003e:/site/static \\\n    -v \u003cpath to config file\u003e:/site/config/site.yaml \\\n    -p \u003chost port\u003e:8080 \\\n    braedon/bottler\n```\n\nIf you don't want to mount the config and site files at run time, you can extend the image with your own Dockerfile that copies them in at build time:\n```docker\nFROM braedon/bottler\n\nCOPY \u003cpath to static files\u003e /site/static\nCOPY \u003cpath to config file\u003e /site/config/site.yaml\n```\n\n# CLI Options\n\nBottler accepts a few CLI flags:\n```\nUsage: main.py [OPTIONS]\n\nOptions:\n  -c, --config-file FILE     Path to the site config file. Can be absolute, or\n                             relative to the current working directory.\n                             (default: config/site.yaml)\n\n  -f, --file-root DIRECTORY  Path to the directory to serve files from. Can be\n                             absolute, or relative to the current working\n                             directory. (default: static)\n\n  -p, --port INTEGER         Port to serve on. (default=8080)\n  --shutdown-sleep INTEGER   How many seconds to sleep during graceful\n                             shutdown. (default=10)\n\n  --shutdown-wait INTEGER    How many seconds to wait for active connections\n                             to close during graceful shutdown (after\n                             sleeping). (default=10)\n\n  -j, --json                 Log in json.\n  -v, --verbose              Log debug messages.\n  -h, --help                 Show this message and exit.\n\n```\n\nAny options placed after the image name (`bottler`) will be passed to the process inside the container. For example, enable structured logging with `--json`:\n```bash\n\u003e sudo docker run --rm \\\n    -v \u003cpath to static files\u003e:/site/static \\\n    -p \u003chost port\u003e:8080 \\\n    braedon/bottler --json\n```\n\nThese options can also be set via environment variables. The environment variable names are prefixed with `BOTTLER_OPT`, e.g. `BOTTLER_OPT_CONFIG_FILE=mysite.yaml` is equivalent to `--config-file mysite.yaml`. CLI options take precedence over environment variables.\n\n# Site Config File\n\nThe config file uses the YAML format.\n\n```yaml\n# The file to serve at directory roots (e.g. `/`, `/docs/`).\n# Can be absolute, or relative to the global file root.\n# Defaults to `index.html`.\nindexFile: index.html\n# The file to use for 404 responses.\n# Can be absolute, or relative to the global file root.\n# Defaults to the standard bottle error page - it's recommended you override it.\nnotFoundFile: 404.html\n\n# Extra headers to return when serving files.\n# The supported headers and their default values are shown here.\n# Setting a header to `false` removes it.\nextraHeaders:\n  Cache-Control: max-age=600\n  Access-Control-Allow-Origin: false\n\n# Configure security headers to return when serving files.\n# The supported headers and their default values are shown here.\n# Setting a header to `false` removes it.\nsecurityHeaders:\n  Strict-Transport-Security: max-age=63072000; includeSubDomains; preload\n  Expect-CT: max-age=86400, enforce\n  Referrer-Policy: no-referrer, strict-origin-when-cross-origin\n  Cross-Origin-Opener-Policy: same-origin\n  Cross-Origin-Embedder-Policy: require-corp\n  Cross-Origin-Resource-Policy: same-origin\n  X-XSS-Protection: 1; mode=block\n  X-Content-Type-Options: nosniff\n  X-Frame-Options: DENY\n\n# Configure policy directives for the `Content-Security-Policy` header.\n# The default directives are shown here.\n# Setting a directive to `false` removes it.\ncontentSecurityPolicy:\n  # Values that include quotes need to be explicitly quoted.\n  default-src: \"'none'\"\n  base-uri: \"'none'\"\n  form-action: \"'none'\"\n  frame-ancestors: \"'none'\"\n  # Some directives don't have values. Set them to `true` to include them.\n  block-all-mixed-content: true\n\n# Configure policy directives for the `Permissions-Policy` header.\n# The `Feature-Policy` header is also set for compatibility.\n# The default directives are shown here.\n# Setting a directive to `false` removes it.\npermissionsPolicy:\n  accelerometer: ()\n  ambient-light-sensor: ()\n  autoplay: ()\n  battery: ()\n  camera: ()\n  display-capture: ()\n  document-domain: ()\n  encrypted-media: ()\n  execution-while-not-rendered: ()\n  execution-while-out-of-viewport: ()\n  fullscreen: ()\n  geolocation: ()\n  gyroscope: ()\n  interest-cohort: ()\n  layout-animations: ()\n  legacy-image-formats: ()\n  magnetometer: ()\n  microphone: ()\n  midi: ()\n  navigation-override: ()\n  oversized-images: ()\n  payment: ()\n  picture-in-picture: ()\n  publickey-credentials-get: ()\n  screen-wake-lock: ()\n  sync-xhr: ()\n  usb: ()\n  wake-lock: ()\n  web-share: ()\n  xr-spatial-tracking: ()\n\n# Extra routes to serve.\n# Incoming requests are checked against the routes in order, and handled by the\n# first to match.\n# If a request doesn't match any routes it's handled as a normal file request.\n#\n# Routes are matched based on their HTTP method and path.\n# `directory` type routes match on a path prefix, while other types match an\n# exact path.\nroutes:\n    # The following route options are available to all route types:\n\n    # The method the request must be using to match the route.\n    # A list can be provided to match multiple methods.\n    # Defaults to `GET`.\n  - method: GET\n    # Response header configs can be overridden for the route.\n    # Only changes need to be specified here - they will be merged with the\n    # global configs.\n    extraHeaders: {}\n    securityHeaders: {}\n    contentSecurityPolicy: {}\n    permissionsPolicy: {}\n\n    # The following route options are specific to the route type:\n\n    # Serve files from a path prefix.\n    type: directory\n    # Requests with a path starting with this prefix are match this route.\n    # Must start and end with a `/`.\n    pathPrefix: /images/\n    # The directory containing the files to serve from this path prefix.\n    # Can be absolute, or relative to the global file root.\n    # Defaults to the global file root plus the path prefix.\n    fileRoot: static\n    # The file to serve at directory roots (e.g. `/images/`, `/images/small/`).\n    # Defaults to `index.html`\n    indexFile: index.html\n\n    # Serve a single file from a path.\n  - type: file\n    # Requests with this exact path match this route.\n    # Must start with '/'.\n    path: /favicon.ico\n    # The Content-Type header to use.\n    # Defaults to guessing from the file extension.\n    contentType: text/html\n    # The file to serve at this path.\n    # Can be absolute, or relative to the global file root.\n    # Defaults to the global file root plus the path.\n    file: images/favicon.ico\n\n    # Serve configured JSON at a path.\n  - type: json\n    # Requests with this exact path match this route.\n    # Must start with '/'.\n    path: /data.json\n    # The data to return as JSON.\n    json: {'foo': 'bar'}\n\n    # Serve configured text at a path.\n  - type: text\n    # Requests with this exact path match this route.\n    # Must start with '/'.\n    path: /hello.html\n    # The Content-Type header to use.\n    contentType: text/html\n    # The text to return.\n    text: |-\n      \u003c!DOCTYPE html\u003e\n      \u003chtml\u003e\n        \u003chead\u003e\n          \u003ctitle\u003eThis is Hello World page\u003c/title\u003e\n        \u003c/head\u003e\n        \u003cbody\u003e\n          \u003ch1\u003eHello World\u003c/h1\u003e\n        \u003c/body\u003e\n      \u003c/html\u003e\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbraedon%2Fbottler","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbraedon%2Fbottler","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbraedon%2Fbottler/lists"}