{"id":51507965,"url":"https://github.com/explicit-logic/python-module-14.5","last_synced_at":"2026-07-08T02:30:44.042Z","repository":{"id":361683957,"uuid":"1254484544","full_name":"explicit-logic/python-module-14.5","owner":"explicit-logic","description":"Website Monitoring and Recovery","archived":false,"fork":false,"pushed_at":"2026-05-31T19:18:31.000Z","size":2085,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-31T21:13:55.377Z","etag":null,"topics":["boto3","devops-bootcamp","digitalocean","docker","linux","python"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/explicit-logic.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"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,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-30T16:14:32.000Z","updated_at":"2026-05-31T19:24:07.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/explicit-logic/python-module-14.5","commit_stats":null,"previous_names":["explicit-logic/python-module-14.5"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/explicit-logic/python-module-14.5","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/explicit-logic%2Fpython-module-14.5","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/explicit-logic%2Fpython-module-14.5/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/explicit-logic%2Fpython-module-14.5/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/explicit-logic%2Fpython-module-14.5/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/explicit-logic","download_url":"https://codeload.github.com/explicit-logic/python-module-14.5/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/explicit-logic%2Fpython-module-14.5/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35249883,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-08T02:00:06.796Z","response_time":61,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"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":["boto3","devops-bootcamp","digitalocean","docker","linux","python"],"created_at":"2026-07-08T02:30:43.473Z","updated_at":"2026-07-08T02:30:44.036Z","avatar_url":"https://github.com/explicit-logic.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Module 14 - Automation with Python\n\nThis repository contains a demo project created as part of my **DevOps studies** in the [TechWorld with Nana – DevOps Bootcamp](https://www.techworld-with-nana.com/devops-bootcamp).\n\n**Demo Project:** Website Monitoring and Recovery\n\n**Technologies used:** Python, DigitalOcean, Docker, Linux\n\n**Project Description:**\n\n- Create a server on a cloud platform\n- Install Docker and run a Docker container on the remote server\n- Write a Python script that monitors the website by accessing it and validating the HTTP response\n- Write a Python script that sends an email notification when website is down\n- Write a Python script that automatically restarts the application \u0026 server when the application is down\n\n---\n\n## Prerequisites\n\n- [Python 3.14+](https://www.python.org/downloads/) and [uv](https://docs.astral.sh/uv/) for dependency management\n- A [DigitalOcean](https://www.digitalocean.com/) account with permission to create droplets and generate API tokens\n- A [Resend](https://resend.com/) account for sending email notifications over SMTP\n- An SSH key pair on your machine — the public key is added to the droplet, and the private key lets the script reconnect to restart the container\n\nThe script relies on the following Python libraries (installed automatically by `uv sync`):\n\n| Library | Purpose |\n| --- | --- |\n| [`requests`](https://requests.readthedocs.io/) | Send the HTTP health-check request to the website |\n| [`python-dotenv`](https://pypi.org/project/python-dotenv/) | Load configuration from the `.env` file |\n| [`paramiko`](https://www.paramiko.org/) | Open an SSH session to the droplet and restart the container |\n| [`pydo`](https://pydo.readthedocs.io/) | DigitalOcean API client used to reboot the droplet |\n| [`schedule`](https://schedule.readthedocs.io/) | Run the monitoring check on a recurring interval |\n\nInstall the dependencies:\n\n```shell\nuv sync\n```\n\n### Configure environment variables\n\nCopy the example file and fill in your own values:\n\n```shell\ncp .env.example .env\n```\n\n| Variable | Description |\n| --- | --- |\n| `DROPLET_IP` | Public IP address of the droplet being monitored |\n| `DROPLET_ID` | Numeric droplet ID, used by the DigitalOcean API to trigger a reboot |\n| `DO_API_TOKEN` | DigitalOcean **read/write** API token |\n| `SMTP_USER` | Resend SMTP username (`resend`) |\n| `SMTP_PASSWORD` | Resend API key |\n| `SSH_PRIVATE_KEY_PATH` | Absolute path to the private SSH key that connects to the droplet |\n\n---\n\n### Overview\n\n![](./images/overview.png)\n\nThe monitor periodically requests the website and reacts to failures in two tiers:\n\n- **Application down** (the site returns a non-`200` status): email an alert and restart the Docker container over SSH.\n- **Server unreachable** (the request fails entirely): email an alert, reboot the whole droplet via the DigitalOcean API, wait for it to come back online, then restart the container.\n\n### 1. Create a droplet on DigitalOcean\n\n- **RAM:** 2 GB\n- Assign the public SSH key from your computer so you can connect without a password.\n\n![](./images/create-droplet.png)\n\nConnect to the droplet:\n\n```sh\nssh root@\u003cDROPLET-IP\u003e\n```\n\nCheck the OS:\n\n```sh\ncat /etc/os-release\n```\n\n![](./images/os-release.png)\n\n### 2. Install Docker and run a container on the remote server\n\nInstall Docker by following the official guide for Ubuntu:\nhttps://docs.docker.com/engine/install/ubuntu\n\nRun the `nginx` image, mapping container port `80` to host port `8080`:\n\n```sh\ndocker run -d -p 8080:80 nginx\n```\n\nOpen `http://\u003cDROPLET-IP\u003e:8080` in your browser to confirm it is serving:\n\n![](./images/nginx.png)\n\n### 3. Monitor the website with an HTTP health check\n\nSend a request to the site and validate the response status code:\n\n```py\nimport os\nimport requests\nfrom dotenv import load_dotenv\n\nload_dotenv()\n\ndroplet_ip = os.getenv('DROPLET_IP')\nresponse = requests.get(f\"http://{droplet_ip}:8080\")\n\nif response.status_code == 200:\n  print('Application is running successfully!')\nelse:\n  print('Application Down. Fix it!')\n```\n\nRun it:\n\n```sh\npython3 monitor-website.py\n```\n\n![](./images/monitor-app.png)\n\n### 4. Send an email notification when the site is down\n\nCreate a [Resend](https://resend.com/) account, then grab your SMTP credentials and an API key:\n\n- https://resend.com/settings/smtp\n- https://resend.com/api-keys\n\n![](./images/resend-api-key.png)\n\nSet the SMTP credentials in `.env`:\n\n```conf\nSMTP_USER=resend\nSMTP_PASSWORD=YOUR_API_KEY\n```\n\nSend an email through Resend's SMTP server whenever the check fails:\n\n```python\nimport os\nimport requests\nimport smtplib\nfrom dotenv import load_dotenv\n\nload_dotenv()\n\nDROPLET_IP = os.getenv('DROPLET_IP')\nSMTP_USER = os.getenv('SMTP_USER')\nSMTP_PASSWORD = os.getenv('SMTP_PASSWORD')\nEMAIL_FROM = 'onboarding@resend.dev'\nEMAIL_TO = EMAIL_FROM\nresponse = requests.get(f\"http://{DROPLET_IP}:8080\")\n\nwith smtplib.SMTP('smtp.resend.com', 587) as smtp:\n  smtp.starttls()\n  smtp.ehlo()\n  smtp.login(SMTP_USER, SMTP_PASSWORD)\n  msg = f\"From: {EMAIL_FROM}\\nTo: {EMAIL_TO}\\nSubject: SITE DOWN\\n\\nFix the issue! Restart the application.\"\n  smtp.sendmail(EMAIL_FROM, EMAIL_TO, msg)\n```\n\nRun the script and check the Resend dashboard to confirm the email was delivered:\n\n```sh\npython3 monitor-website.py\n```\n\n![](./images/email-delivered.png)\n\nTo simulate an outage, stop the `nginx` container on the droplet:\n\n```sh\ndocker ps\ndocker stop \u003cNGINX-CONTAINER-ID\u003e\n```\n\n#### Handle connection errors\n\nA stopped container makes the site unreachable, so `requests.get` raises an exception instead of returning a status code. Wrap the check in `try/except` and move the email logic into a reusable `send_notification` function:\n\n```py\nimport os\nimport requests\nimport smtplib\nfrom dotenv import load_dotenv\n\nload_dotenv()\n\nDROPLET_IP = os.getenv('DROPLET_IP')\nSMTP_USER = os.getenv('SMTP_USER')\nSMTP_PASSWORD = os.getenv('SMTP_PASSWORD')\nEMAIL_FROM = 'onboarding@resend.dev'\nEMAIL_TO = EMAIL_FROM\n\ndef send_notification(email_msg):\n  with smtplib.SMTP('smtp.resend.com', 587) as smtp:\n      smtp.starttls()\n      smtp.ehlo()\n      smtp.login(SMTP_USER, SMTP_PASSWORD)\n      message = f\"From: {EMAIL_FROM}\\nTo: {EMAIL_TO}\\nSubject: SITE DOWN\\n\\n {email_msg}\"\n      smtp.sendmail(EMAIL_FROM, EMAIL_TO, message)\n\ntry:\n  response = requests.get(f\"http://{DROPLET_IP}:8080\")\n  if response.status_code == 200:\n    print('Application is running successfully!')\n  else:\n    print('Application Down. Fix it!')\n    msg = f\"Application returned {response.status_code}.\"\n    send_notification(msg)\nexcept Exception as ex:\n  print(f\"Connection error happened: {ex}\")\n  msg = f\"Application not accessible at all.\"\n  send_notification(msg)\n```\n\nRun again — this time the request fails outright, and you receive the \"not accessible\" email:\n\n```sh\npython3 monitor-website.py\n```\n\n![](./images/not-accessible-email.png)\n\n### 5. Automatically restart the application and server\n\nNow react to failures instead of only reporting them.\n\nAdd the absolute path of your private SSH key to `SSH_PRIVATE_KEY_PATH` in `.env`, then verify the script can open an SSH session to the droplet:\n\n```py\nssh = paramiko.SSHClient()\nssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())\nssh.connect(hostname=DROPLET_IP, username='root', key_filename=SSH_PRIVATE_KEY_PATH)\nstdin, stdout, stderr = ssh.exec_command('docker ps')\nprint(stdout.readlines())\nssh.close()\n```\n\n![](./images/check-ssh-connect.png)\n\nWith SSH working, the script can find the stopped `nginx` container and start it again — see `restart_container()` in the [final code](./monitor-website.py).\n\n#### Reboot the droplet when it is completely unreachable\n\nIf the server itself is down, SSH cannot connect either — so reboot the whole droplet through the DigitalOcean API.\n\nCreate a **read/write** API token:\nhttps://cloud.digitalocean.com/account/api/tokens/new\n\n![](./images/do-token.png)\n\nSet the token as `DO_API_TOKEN` in `.env`.\n\nFind your droplet ID — open the droplet in the DigitalOcean dashboard and read the number from the URL:\n\n```\nhttps://cloud.digitalocean.com/droplets/123456789\n                                        ^^^^^^^^^\n```\n\nSet that number as `DROPLET_ID` in `.env`.\n\nTrigger a reboot with `pydo`:\n\n```py\nclient = pydo.Client(token=DO_API_TOKEN)\nclient.droplet_actions.post(DROPLET_ID, body={'type': 'reboot'})\n```\n\nThe final script goes a step further: it polls the reboot action until it completes, waits for the droplet status to become `active` again, and only then restarts the container — see `restart_server_and_container()` in the [final code](./monitor-website.py).\n\nTo test it, stop the container (or power off the droplet) and run the script:\n\n```sh\ndocker ps\ndocker stop \u003cNGINX-CONTAINER-ID\u003e\n```\n\nWatch the reboot appear on your droplet dashboard:\n\n![](./images/droplet-reboot.png)\n\n### 6. Run the check on a schedule\n\nFinally, tie everything together by running the monitor every 5 minutes in a loop:\n\n```py\nimport schedule\nimport time\n\nschedule.every(5).minutes.do(monitor_application)\n\nwhile True:\n  schedule.run_pending()\n  time.sleep(1)\n```\n\nStart the monitor:\n\n```sh\npython3 monitor-website.py\n```\n\nSee the complete script: [monitor-website.py](./monitor-website.py)\n\n![](./images/demo.gif)\n\n### Remove created droplet on DigitalOcean\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fexplicit-logic%2Fpython-module-14.5","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fexplicit-logic%2Fpython-module-14.5","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fexplicit-logic%2Fpython-module-14.5/lists"}