diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 0000000..91407a5 --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,61 @@ +# Sample workflow for building and deploying a Jekyll site to GitHub Pages +name: Deploy Jekyll site to Pages + +on: + push: + branches: ["main"] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +# Allow one concurrent deployment +concurrency: + group: "pages" + cancel-in-progress: true + +jobs: + # Build job + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.3' # Not needed with a .ruby-version file + bundler-cache: true # runs 'bundle install' and caches installed gems automatically + cache-version: 0 # Increment this number if you need to re-download cached gems + working-directory: docs + - name: Setup Pages + id: pages + uses: actions/configure-pages@v5 + - name: Build with Jekyll + working-directory: docs + # Outputs to the './_site' directory by default + run: bundle exec jekyll build --baseurl "${{ steps.pages.outputs.base_path }}" + env: + JEKYLL_ENV: production + - name: Upload artifact + # Automatically uploads an artifact from the './_site' directory by default + uses: actions/upload-pages-artifact@v3 + with: + path: docs/_site + + # Deployment job + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + needs: build + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/README.md b/README.md index 3196bfd..2f6e2e7 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ Options: - `--wait-after-healthy SECONDS` - (not required) - Time to wait after new container is healthy before removing old container. Works when healthcheck is defined. Default: 0 - `--env-file FILE` - (not required) - Path to env file, can be specified multiple times, as in `docker compose`. -See examples in [examples](examples) directory for sample `docker-compose.yml` files. +See [examples](docs/examples) for sample `docker-compose.yml` files. ### ⚠️ Caveats diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 0000000..96ec9c3 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,9 @@ +# Ignore the default location of the built site, and caches and metadata generated by Jekyll +_site/ +.sass-cache/ +.jekyll-cache/ +.jekyll-metadata + +# Ignore folders generated by Bundler +.bundle/ +vendor/ diff --git a/docs/404.md b/docs/404.md new file mode 100644 index 0000000..7f8b905 --- /dev/null +++ b/docs/404.md @@ -0,0 +1,10 @@ +--- +permalink: /404.html +--- + +## 404 - Page Not Found + +Return to the [home page](/). + + + diff --git a/docs/Gemfile b/docs/Gemfile new file mode 100644 index 0000000..c84c0ce --- /dev/null +++ b/docs/Gemfile @@ -0,0 +1,8 @@ +source 'https://rubygems.org' + +gem "jekyll", "~> 4.3.4" # installed by `gem jekyll` +# gem "webrick" # required when using Ruby >= 3 and Jekyll <= 4.2.2 + +gem "just-the-docs", "0.10.0" # pinned to the current release +# gem "just-the-docs" # always download the latest release +gem "jekyll-default-layout" diff --git a/docs/Gemfile.lock b/docs/Gemfile.lock new file mode 100644 index 0000000..2217ae3 --- /dev/null +++ b/docs/Gemfile.lock @@ -0,0 +1,94 @@ +GEM + remote: https://rubygems.org/ + specs: + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) + bigdecimal (3.1.8) + colorator (1.1.0) + concurrent-ruby (1.3.4) + em-websocket (0.5.3) + eventmachine (>= 0.12.9) + http_parser.rb (~> 0) + eventmachine (1.2.7) + ffi (1.17.0-arm64-darwin) + ffi (1.17.0-x86_64-linux-gnu) + forwardable-extended (2.6.0) + google-protobuf (4.28.1-arm64-darwin) + bigdecimal + rake (>= 13) + google-protobuf (4.28.1-x86_64-linux) + bigdecimal + rake (>= 13) + http_parser.rb (0.8.0) + i18n (1.14.6) + concurrent-ruby (~> 1.0) + jekyll (4.3.4) + addressable (~> 2.4) + colorator (~> 1.0) + em-websocket (~> 0.5) + i18n (~> 1.0) + jekyll-sass-converter (>= 2.0, < 4.0) + jekyll-watch (~> 2.0) + kramdown (~> 2.3, >= 2.3.1) + kramdown-parser-gfm (~> 1.0) + liquid (~> 4.0) + mercenary (>= 0.3.6, < 0.5) + pathutil (~> 0.9) + rouge (>= 3.0, < 5.0) + safe_yaml (~> 1.0) + terminal-table (>= 1.8, < 4.0) + webrick (~> 1.7) + jekyll-default-layout (0.1.5) + jekyll (>= 3.0, < 5.0) + jekyll-include-cache (0.2.1) + jekyll (>= 3.7, < 5.0) + jekyll-sass-converter (3.0.0) + sass-embedded (~> 1.54) + jekyll-seo-tag (2.8.0) + jekyll (>= 3.8, < 5.0) + jekyll-watch (2.2.1) + listen (~> 3.0) + just-the-docs (0.10.0) + jekyll (>= 3.8.5) + jekyll-include-cache + jekyll-seo-tag (>= 2.0) + rake (>= 12.3.1) + kramdown (2.4.0) + rexml + kramdown-parser-gfm (1.1.0) + kramdown (~> 2.0) + liquid (4.0.4) + listen (3.9.0) + rb-fsevent (~> 0.10, >= 0.10.3) + rb-inotify (~> 0.9, >= 0.9.10) + mercenary (0.4.0) + pathutil (0.16.2) + forwardable-extended (~> 2.6) + public_suffix (6.0.1) + rake (13.2.1) + rb-fsevent (0.11.2) + rb-inotify (0.11.1) + ffi (~> 1.0) + rexml (3.3.7) + rouge (4.3.0) + safe_yaml (1.0.5) + sass-embedded (1.78.0-arm64-darwin) + google-protobuf (~> 4.27) + sass-embedded (1.78.0-x86_64-linux-gnu) + google-protobuf (~> 4.27) + terminal-table (3.0.2) + unicode-display_width (>= 1.1.1, < 3) + unicode-display_width (2.6.0) + webrick (1.8.1) + +PLATFORMS + arm64-darwin + x86_64-linux-gnu + +DEPENDENCIES + jekyll (~> 4.3.4) + jekyll-default-layout + just-the-docs (= 0.10.0) + +BUNDLED WITH + 2.5.9 diff --git a/docs/_config.yml b/docs/_config.yml new file mode 100644 index 0000000..02e2633 --- /dev/null +++ b/docs/_config.yml @@ -0,0 +1,22 @@ +title: docker rollout +description: 🚀 Zero Downtime Deployment for Docker Compose. +theme: just-the-docs +url: https://docker-rollout.wowu.dev + +# just-the-docs theme config +aux_links: + GitHub Repository: https://github.com/wowu/docker-rollout +nav_external_links: + - title: GitHub + url: https://github.com/wowu/docker-rollout + hide_icon: false # set to true to hide the external link icon - defaults to false + opens_in_new_tab: false # set to true to open this link in a new tab - defaults to false +callouts: + warning: + title: Warning + color: red +search: + focus_shortcut_key: 'k' + +plugins: + - jekyll-default-layout diff --git a/docs/_includes/head_custom.html b/docs/_includes/head_custom.html new file mode 100644 index 0000000..c0dbb6a --- /dev/null +++ b/docs/_includes/head_custom.html @@ -0,0 +1,2 @@ + + diff --git a/docs/cli-options.md b/docs/cli-options.md new file mode 100644 index 0000000..bcdb521 --- /dev/null +++ b/docs/cli-options.md @@ -0,0 +1,102 @@ +--- +title: CLI Options +nav_order: 3 +--- + +# CLI Options +{: .no_toc } + +1. TOC +{:toc} + + +## Docker flags + +All docker flags can be used with `docker rollout` normally, like `--context`, `--env`, `--log-level`, etc. + +```bash +docker --context my-remote-context rollout +``` + +The plugin flags are described below. + +## `-f | --file FILE` + +Path to compose file, can be specified multiple times, as in `docker compose`. + +**Example** + +Single file: + +```bash +docker rollout -f docker-compose.yml +``` + +With override file: + +```bash +docker rollout -f docker-compose.yml -f docker-compose.override.yml +``` + +## `-t | --timeout SECONDS` + +Timeout in seconds to wait for new container to become healthy, if the container has healthcheck defined. + +Default: 60 + +**Example** + +Decrease timeout to 30 seconds: + +```bash +docker rollout --timeout 30 +``` + +## `-w | --wait SECONDS` + +Time to wait for new container to be ready if healthcheck is not defined. + +Default: 10 + +**Example** + +Increase wait time to 30 seconds for a service that takes longer to start: + +```bash +docker rollout --wait 30 +``` + +## `--wait-after-healthy SECONDS` + +Time to wait after new container is healthy before removing old container. Works when a healthcheck is defined. Can be useful if the service healthcheck is not reliable and the service needs some time to stabilize (see [#27](https://github.com/wowu/docker-rollout/issues/27)). + +Default: 0 + +**Example** + +Wait 10 seconds after a new container is healthy before terminating the old container: + +```bash +docker rollout --wait-after-healthy 10 +``` + +## `--env-file FILE` + +Path to env file, can be specified multiple times, like in `docker compose`. + +See [Docker Compose documentation](https://docs.docker.com/reference/cli/docker/compose/). + +**Example** + +Single env file: + +```bash +docker rollout --env-file .env +``` + +Multiple env files: + +```bash +docker rollout --env-file .env --env-file .env.prod +``` + diff --git a/docs/examples/index.md b/docs/examples/index.md new file mode 100644 index 0000000..7563262 --- /dev/null +++ b/docs/examples/index.md @@ -0,0 +1,6 @@ +--- +title: Examples +--- + +# Examples + diff --git a/docs/examples/nginx_proxy.md b/docs/examples/nginx_proxy.md new file mode 100644 index 0000000..250e4fd --- /dev/null +++ b/docs/examples/nginx_proxy.md @@ -0,0 +1,43 @@ +--- +title: Nginx Proxy +parent: Examples +--- + +# Nginx Proxy + +Works with Docker Compose v2. + +## Compose file + +```yml +services: + whoami: + image: jwilder/whoami + environment: + - VIRTUAL_HOST=whoami.example.com + + nginx-proxy: + image: nginxproxy/nginx-proxy + ports: + - "80:80" + volumes: + - /var/run/docker.sock:/tmp/docker.sock:ro +``` + +## Steps + +1. Change domain in `docker-compose.yml` to a domain pointing to your server + +2. Start all services + + ```bash + docker-compose up -d + ``` + +3. Change `whoami` image to, for example, `traefik/whoami`. + +4. Deploy a new version of `whoami` service without downtime. + + ```bash + docker rollout whoami + ``` diff --git a/examples/traefik/docker-compose.yml b/docs/examples/traefik.md similarity index 58% rename from examples/traefik/docker-compose.yml rename to docs/examples/traefik.md index b657b97..2123f91 100644 --- a/examples/traefik/docker-compose.yml +++ b/docs/examples/traefik.md @@ -1,5 +1,13 @@ -version: "3.7" +--- +title: Traefik +parent: Examples +--- +# Traefik + +## Compose file + +```yml services: whoami: image: traefik/whoami @@ -21,3 +29,22 @@ services: - "8080:8080" volumes: - "/var/run/docker.sock:/var/run/docker.sock:ro" +``` + +## Steps + +1. Change domain in `docker-compose.yml` to a domain pointing to your server + +2. Start all services + + ```bash + docker-compose up -d + ``` + +3. Change `whoami` image to, for example, `jwilder/whoami` + +4. Deploy new version of `whoami` service without downtime + + ```bash + docker rollout whoami + ``` diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 0000000..2a3825a --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,118 @@ +--- +title: Getting Started +nav_order: 2 +--- + +# Getting Started +{: .no_toc } + +1. TOC +{:toc} + +## Install docker rollout + +_docker rollout_ is a single bash script that lives in docker cli plugins directory. It does not require any additional dependencies to work. To install _docker rollout_: + +1. Create Docker cli plugins directory if it does not exist: + + ```bash + mkdir -p ~/.docker/cli-plugins + ``` + +2. Download the docker rollout script to Docker cli plugins directory: + + ```bash + curl https://raw.githubusercontent.com/wowu/docker-rollout/master/docker-rollout -o ~/.docker/cli-plugins/docker-rollout + ``` + + You can also download it manually from the [latest release page](https://github.com/wowu/docker-rollout/releases/latest), review the script content, and save it to `~/.docker/cli-plugins/docker-rollout`. + +3. Make the plugin executable: + + ```bash + chmod +x ~/.docker/cli-plugins/docker-rollout + ``` + +4. Verify that the plugin is available: + + ```bash + docker rollout --help + #=> Usage: docker rollout [OPTIONS] SERVICE + #=> ... + ``` + +## Prepare your service + +_docker rollout_ requires a Docker Compose file to work. Traffic to your service must be handled by a reverse proxy with automatic service discovery like [Traefik](https://github.com/traefik/traefik) or [nginx-proxy](https://github.com/nginx-proxy/nginx-proxy). Here is a sample Compose file with Traefik reverse proxy and a service: + +```yaml +services: + my-app: + image: my-app:latest + labels: + - "traefik.enable=true" + - "traefik.http.routers.my-app.entrypoints=web" + - "traefik.http.routers.my-app.rule=Host(`my-app.example.com`)" + + traefik: + image: traefik:v2.9 + command: + - "--providers.docker=true" + - "--providers.docker.exposedbydefault=false" + - "--entrypoints.web.address=:80" + ports: + - "80:80" + volumes: + - "/var/run/docker.sock:/var/run/docker.sock:ro" +``` + +{: .warning } +Your service cannot expose any `ports`, use `network_mode: host`, or have a defined `container_name`. These options will prevent _docker rollout_ from starting multiple instances of the same service. + +Check out the [Examples](examples) section for more sample Compose files. + +## Start all services + +Firstly start all services. docker rollout will be used later to update the service. + +```bash +docker compose up -d +``` + +## Deploy your service without downtime + +Now after modifying the service (e.g. changing the image, rebuiling the image, changing envionment variables) **instead of** running `docker compose up -d` to update the service without downtime run `docker rollout`: + +```bash +docker rollout my-app +``` + +You can also specify the path to the Compose file with `-f` option, just like with `docker compose`: + +```bash +docker rollout -f docker-compose.yml my-app +``` + + +This command will scale the service to two instances, wait for the new container to be ready, and then remove the old container. If you run more that one instance of the service, for example using `docker compose scale` or `replicas` in the Compose file, _docker rollout_ will scale the service to twice the current number of instances. + +If your service has a healthcheck defined, _docker rollout_ will wait for the new containers to become healthy before removing the old ones. Reverse proxy like Traefik or nginx-proxy will route traffic to the new container automatically, after it becomes healthy. + +See [CLI Options](options) for the list of all available options. + +## Deployment script + +The recommended way of using *docker rollout* is to put it in your deployment script. For example, here is a sample deployment script for a service that is updated by pulling the latest code from a git repository, building a new image, running database migrations, and deploying the new version: + +```bash +# Download latest code +git pull +# Build new app image +docker compose build web +# Run database migrations +docker compose run web rake db:migrate +# Deploy new version +docker rollout web +``` + +Usually `docker rollout ` will be a drop-in replacement for `docker compose up -d ` in your deployment scripts. diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..21d1f2f --- /dev/null +++ b/docs/index.md @@ -0,0 +1,86 @@ +--- +title: Home +nav_order: 1 +--- + +# docker rollout - Docker CLI plugin for updating Docker Compose services without downtime + +Simply replace `docker compose up -d ` with `docker rollout ` in your deployment scripts to update a service without downtime. +{: .fs-5 .fw-300 } + +[Get started](#installation){: .btn .btn-primary .fs-5 .mb-4 .mb-md-0 .mr-2 } +[View on GitHub](https://github.com/wowu/docker-rollout){: .btn .fs-5 .mb-4 .mb-md-0} + +## Features + +- ⏳ Zero downtime deployment for Docker Compose services +- 🐳 Works with Docker Compose v2 and `docker-compose` v1 ([What's the difference?](docker_compose_versions)) +- ❤️ Supports Docker healthchecks out of the box + +## How does it work? + +This command will scale the service to twice the current number of instances, wait for the new containers to be ready, and then remove the old containers. + +## Why is it useful? + +Using `docker compose up` to deploy a new version of a service causes downtime because the app container is stopped before the new container is created. If your application takes a while to boot, this may be noticeable to your users. + +## Caveats (⚠️ read before using) + +- Your service cannot have `container_name` and `ports` defined in `docker-compose.yml`, as it's not possible to run multiple containers with the same name or port mapping. Use a proxy as described below. +- Proxy like [Traefik](https://github.com/traefik/traefik) or [nginx-proxy](https://github.com/nginx-proxy/nginx-proxy) is required to route traffic to the containers. Refer to the [Examples](examples) for sample compose files. +- Each deployment will increment the index in container name (e.g. `project-web-1` -> `project-web-2`). + +## Installation + +Quick install: + +```bash +# Create directory for Docker cli plugins +mkdir -p ~/.docker/cli-plugins +# Download docker-rollout script to Docker cli plugins directory +curl https://raw.githubusercontent.com/wowu/docker-rollout/master/docker-rollout -o ~/.docker/cli-plugins/docker-rollout +# Make the script executable +chmod +x ~/.docker/cli-plugins/docker-rollout +``` + +## Usage + +Run `docker rollout ` instead of `docker compose up -d ` to update a service without downtime. If you have both `docker compose` plugin and `docker-compose` command available, docker-rollout will use `docker compose` by default. + +```bash +docker rollout -f docker-compose.yml +``` + +You can read read more about the setup in [Getting Started](getting-started), see [available cli options](cli-options), and check out some [examples](examples). + +### Sample deployment script + +Sample deployment script for `web` service: + +```bash +# Download latest code +git pull +# Build new app image +docker compose build web +# Run database migrations +docker compose run web rake db:migrate +# Deploy new version +docker rollout web +``` + +## Rationale and alternatives + +Using `docker compose up` to deploy a new version of a service causes downtime because the app container is stopped before the new container is created. +If your application takes a while to boot, this may be noticeable to users. + +Using container orchestration tools like [Kubernetes](https://kubernetes.io/) or [Nomad](https://www.nomadproject.io/) is usually an overkill for projects that will do fine with a single-server Docker Compose setup. [Dokku](https://github.com/dokku/dokku) comes with zero-downtime deployment and more useful features, but it's not as flexible as Docker Compose. + +If you have a proxy like [Traefik](https://github.com/traefik/traefik) or [nginx-proxy](https://github.com/nginx-proxy/nginx-proxy), a zero downtime deployment can be achieved by writing a script that scales the service to 2 instances, waits for the new container to be ready, and then removes the old container. +`docker rollout` does exactly that, but with a single command that you can use in your deployment scripts. +If you're using Docker healthchecks, Traefik will make sure that traffic is only routed to the new container when it's ready. + +## License + +[MIT License](https://github.com/wowu/docker-rollout/blob/main/LICENSE) © Karol Musur + diff --git a/docs/uninstalling.md b/docs/uninstalling.md new file mode 100644 index 0000000..6b55865 --- /dev/null +++ b/docs/uninstalling.md @@ -0,0 +1,14 @@ +--- +title: Uninstalling +nav_order: 5 +--- + +# Uninstalling docker rollout + +Remove the script from Docker cli plugins directory: + +```bash +rm ~/.docker/cli-plugins/docker-rollout +``` + +No other cleanup is required. diff --git a/docs/upgrading.md b/docs/upgrading.md new file mode 100644 index 0000000..249d6b5 --- /dev/null +++ b/docs/upgrading.md @@ -0,0 +1,31 @@ +--- +title: Upgrading +nav_order: 4 +--- + +# Upgrading docker rollout + +All existing Docker Rollout versions are backward compatible. You can upgrade to the latest version by downloading the script again. + +1. Check the current plugin version: + + ```bash + ~/.docker/cli-plugins/docker-rollout docker-cli-plugin-metadata + #=> ... + #=> "Version": "v0.9", + #=> ... + ``` + +2. If new version is available, download the latest version: + + ```bash + curl https://raw.githubusercontent.com/wowu/docker-rollout/master/docker-rollout -o ~/.docker/cli-plugins/docker-rollout + ``` + + You can check the latest version on the [releases page](https://github.com/wowu/docker-rollout/releases). + +3. You may need to make the file executable again: + + ```bash + chmod +x ~/.docker/cli-plugins/docker-rollout + ``` diff --git a/examples/nginx-proxy/README.md b/examples/nginx-proxy/README.md deleted file mode 100644 index 62f6d85..0000000 --- a/examples/nginx-proxy/README.md +++ /dev/null @@ -1,12 +0,0 @@ -# nginx-proxy example - -1. Change domain in `docker-compose.yml` to a domain pointing to your server -2. Start all services - ```bash - docker-compose up -d - ``` -3. Change `whoami` image to, for example, `traefik/whoami` -4. Deploy new version of `whoami` service without downtime - ```bash - docker rollout whoami - ``` diff --git a/examples/nginx-proxy/docker-compose.yml b/examples/nginx-proxy/docker-compose.yml deleted file mode 100644 index da7bc7b..0000000 --- a/examples/nginx-proxy/docker-compose.yml +++ /dev/null @@ -1,14 +0,0 @@ -version: "3.7" - -services: - whoami: - image: jwilder/whoami - environment: - - VIRTUAL_HOST=whoami.example.com - - nginx-proxy: - image: nginxproxy/nginx-proxy - ports: - - "80:80" - volumes: - - /var/run/docker.sock:/tmp/docker.sock:ro diff --git a/examples/traefik/README.md b/examples/traefik/README.md deleted file mode 100644 index 94aaced..0000000 --- a/examples/traefik/README.md +++ /dev/null @@ -1,12 +0,0 @@ -# Traefik example - -1. Change domain in `docker-compose.yml` to a domain pointing to your server -2. Start all services - ```bash - docker-compose up -d - ``` -3. Change `whoami` image to, for example, `jwilder/whoami` -4. Deploy new version of `whoami` service without downtime - ```bash - docker rollout whoami - ```