{"id":19277516,"url":"https://github.com/ruffinweb/devopsguide","last_synced_at":"2025-02-23T21:25:01.316Z","repository":{"id":199172064,"uuid":"702282614","full_name":"ruffinweb/DevOpsGuide","owner":"ruffinweb","description":"Detailed guide for setting up a backend on a VPS.","archived":false,"fork":false,"pushed_at":"2023-10-09T02:27:47.000Z","size":20,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-01-05T15:48:34.768Z","etag":null,"topics":["bash","devops","linux","networking","security","ssh","system-administration"],"latest_commit_sha":null,"homepage":"","language":null,"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/ruffinweb.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}},"created_at":"2023-10-09T02:19:56.000Z","updated_at":"2024-09-22T03:57:17.000Z","dependencies_parsed_at":null,"dependency_job_id":"c5db87d6-4bb1-4247-acc3-cd98d40aa0fa","html_url":"https://github.com/ruffinweb/DevOpsGuide","commit_stats":null,"previous_names":["ruffineli77/devopsguide","ruffinweb/devopsguide"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ruffinweb%2FDevOpsGuide","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ruffinweb%2FDevOpsGuide/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ruffinweb%2FDevOpsGuide/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ruffinweb%2FDevOpsGuide/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ruffinweb","download_url":"https://codeload.github.com/ruffinweb/DevOpsGuide/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":240381053,"owners_count":19792394,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","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":["bash","devops","linux","networking","security","ssh","system-administration"],"created_at":"2024-11-09T21:06:01.245Z","updated_at":"2025-02-23T21:25:01.276Z","avatar_url":"https://github.com/ruffinweb.png","language":null,"funding_links":[],"categories":[],"sub_categories":[],"readme":"# DevOpsGuide\n\nDetailed guide for setting up a project on a VPS.\n\n## Guide Use\n\nThe guide can be split into two sections: Server Setup and Application Setup. \nServer Setup includes the tools and configuration needed for my server and any application it may host. \nApplication Setup includes the tools and configuration needed for my test application to run.\nAn in-depth guide for the development of the application itself can be found [here](https://github.com/ruffineli77/Portfolio-Portal).\n\nThis guide will eventually be turned into a shell script to automate my workflow.\nI want to take extra time to understand what I'm doing before getting into automation for DevOps.\nUntil I take the time to learn a more full-featured editor for my server, I'll be using nano to make quick edits to my files.\n\n## Preparation\n\nMake sure you include these steps or something similar: \n\n- A VPS running Debian 12/Bookworm with root access and IPv4 and IPv6 addresses.\n- A domain name purchased to easily find your site when it is on the Web (I include a section for configuring DNS records).\n\n## Server Setup\n\n### 1. Setup SSH Keys\n\n1. Generate the ssh keys.\n2. Copy the public SSH key to the server.\n3. Connect to the server.\n\n ```bash\n ssh-keygen -t ed25519 -C \"my.email\" -b 4096 -f ~/.ssh/vpsKey1\n ssh-copy-id root@my-servers-public-ip-address\n ssh root@my-servers-public-ip-address\n ```\n\nThe first step of all is setting up SSH keys to securely access my server.\nDebian 12 comes with ssh pre-installed, so installation is not needed typically.\nThe first command generates the private/public ssh key pair, vpsKey1 and vpsKey1.pub. \nThe keys are usually stored in the ~/.ssh folder. \nRunning the ssh-keygen command will create the ~/.ssh folder if it doesn't already exist\nThe email argument works like a comment to show who generated the ssh keys.\nThe second command uses another OpenSSH utility to move the public key to my public server.\nThe last command connects to my server using root login and the new ssh keys.\nWhen I configured my VPS, I used a password to create my root user, so password login is enabled automatically on my server. \nFrom now on, I'll be generating private SSH keys for each public system I have and use those to log in.\nThis eliminates the security risks that arise if someone manages to get access to my root password or any individual server.\n\n### 2. Update the System\n\n1. Update and upgrade the server.\n\n ```bash\n sudo apt update \u0026\u0026 apt full-upgrade\n ```\n\nIts good practice to update a new Unix-like system because new vulnerabilities in the Unix/Linux kernel appear frequently. \nKeeping an updated system ensures these vulnerabilities are addressed whenever possible.\n\n### 3A. Create an Everyday User\n\n1. Create a user.\n2. Grant superuser privileges.\n3. Copy the public SSH key to the new user's .ssh folder.\n\n ```bash\n adduser my-new-username\n usermod -aG sudo my-new-username\n ssh-copy-id my-new-username@my-servers-public-ip-address\n ```\n\nNow I create a new user with a different password than my root account.\nThis new user is given superuser privileges,\nso it can perform restricted commands without granting it complete control over my system.\nThere are two methods of copying my SSH keys to the new user. \nI'll be using the simpler method that uses the ssh-copy-id command.\nIt automatically copies the ssh key from my root users .ssh folder to my new user's folder\nand sets the proper permissions and ownerships.\nIn situations where I need more control over the process, I can manually copy the SSH keys from my \nroot users .ssh folder to my new user's folder.\nIf network restrictions or an incorrect SSH configuration prevent the ssh-copy-id command from running successfully, \nI'll have to use the manual method.\n\n### 3B. Manually Copying an SSH Key\n\nRefer to this section to manually copy the SSH keys.\n\n1. Create an .ssh directory for my new user\n2. Copy the root `.ssh/authorized_keys` file to the new user's `.ssh` folder.\n3. Change the owner of the `.ssh` folder and its contents to the new user.\n4. Give the user permission to read and write the `.ssh/authorized_keys` file.\n5. Set restrictive permissions on the new user's `.ssh` directory.\n\n```bash\n sudo mkdir /home/my-user-name/.ssh\n sudo cp /root/.ssh/authorized_keys /home/my-user-name/.ssh/authorized_keys\n sudo chown $USER:$USER /home/my-user-name/.ssh -R\n sudo chmod 600 /home/my-user-name/.ssh/authorized_keys\n sudo chmod 700 /home/my-user-name/.ssh\n```\n\nWhen I first log into the server via SSH instead of using a root password, a public key already exists in my root account's `.ssh` folder. \nTherefore, password login is not automatically enabled. \nTo enable login for my new user, I need to copy the `authorized_keys` file from the root account to my new user account.\nThe new user is also given the appropriate permissions and ownership, similar to the root account.\n\n### 4. Set up the Firewall (UFW).\n\n1. Display the list of allowed connections.\n2. Add a new Allow rule for my new ssh port, port 80 for my websites HTTP traffic, and 443 for my websites HTTPS traffic.\n3. Enable the firewall.\n4. Display the status of the firewall.\n5. Connect to the server from a new terminal.\n\n ```bash\n sudo ufw status\n sudo ufw allow my-new-port-number \u0026\u0026 sudo ufw allow 80 \u0026\u0026 sudo ufw allow 443\n sudo ufw deny 22\n sudo ufw enable\n sudo ufw status\n ssh my-new-username@my-servers-public-ip-address\n ```\n\nThis sets my firewall to block allow the new port I'll use for SSH, HTTP, and HTTPS. I check the firewall list, \nallow the new port number, deny the default Openssh port, enable the ssh service and check for errors before \nconnecting to the server using my new port number and username. I open up a new terminal window instead of exiting my \ncurrent SSH connection in the same window in case I made a mistake and am unable to log back in. If UFW shows no \nerrors, I can move on. If I am unable to connect or get any errors, I can revert the changes I made. From now on, if any \nadditional services require access to the firewall, I have to add a new Allow rule.\n\n\n### 5. Limit SSH Login\n\n1. Make a backup of the config file.\n2. Open and edit the SSH configuration file.\n3. Restrict connections to a single uncommon port number.\n4. Make the server disconnect if a login attempt takes longer than 60 seconds to occur.\n5. Disable root login.\n6. Public key login is automatically enabled on Debian, so I don't have to uncomment that line.\n7. Disable password login.\n8. Prevent empty passwords from being accepted during log-in.\n9. Set my user as the only one that can access the server.\n10. Set my group as the only one that can access the server.\n11. Save and exit the sshd_config file.\n12. Reload and check the status of the SSHD service and make sure there are no errors.\nI Look for ```Active: active (running)``` followed by the date and time that my SSHD service was started. \nThis tells me that my SSHD service is running.\nIf the sshd service fails to start, then the ```Active``` header would look like this ```Active: failed (Result: exit-code)```\nAny error messages related to why my service did not start, will be shown at the end of the screen.\n\n  ```bash\n sudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak\n sudo nano /etc/ssh/sshd_config\n sudo systemctl reload sshd \n sudo systemctl status sshd\n  ```\n\nAfter running these commands, be sure to check the SSH service status. \nLook for indications that the service is \"active (running)\" to ensure everything is functioning as expected.\n\nAfter making the changes,\nand the sshd service is running successfully, it's important to confirm that I can still access my public server. \nI open a new terminal window and try to connect to my public server with a new port number. \nIf the connection fails, I can revert to the original sshd_config file and correct the errors. \nThis is as simple as removing the edited sshd_config file\nand copying the backup file to the same location with the original file name.\n\nBy disabling the less secure login options and switching to a non-default SSH port, I've significantly reduced the surface area for potential attacks. \nPublic key authentication is one of the most secure methods\nof accessing my server because, assuming my local system is secure,\nI am the only one with the credentials that public server will be looking for.\nHowever, you must securely store your SSH key pairs because they are now the only way to access my public server.\nThe new port number you specified is also the only one that can access my public server.\n\nIn this step, I disable the less secure login options and change the default SSH port for my public server. This means \nusing my ssh key pair is the only way to log in to the server. If either key is destroyed, a new pair must be \ngenerated. Finally, I reload and check the status of the SSH service to make sure it still works.\n\nThis is an example of the sshd_config file with enhanced security.\n\n```bash\nInclude /etc/ssh/sshd_config.d/*.conf\n\nAllowUsers my-user-name\nAllowGroups my-group-name\n\nPort my-new-port-number\n#AddressFamily any\n#ListenAddress 0.0.0.0\n#ListenAddress ::\n\n#HostKey /etc/ssh/ssh_host_rsa_key\n#HostKey /etc/ssh/ssh_host_ecdsa_key\n#HostKey /etc/ssh/ssh_host_ed25519_key\n\n# Ciphers and keying\n#RekeyLimit default none\n\n# Logging\n#SyslogFacility AUTH\n#LogLevel INFO\n\n# Authentication:\n\nLoginGraceTime  60\nPermitRootLogin no\n#StrictModes yes\n#MaxAuthTries 6\n#MaxSessions 10\n\n# PubkeyAuthentication yes\n\n# Expect .ssh/authorized_keys2 to be disregarded by default in future.\n#AuthorizedKeysFile\t.ssh/authorized_keys .ssh/authorized_keys2\n\n#AuthorizedPrincipalsFile none\n\n#AuthorizedKeysCommand none\n#AuthorizedKeysCommandUser nobody\n\n# For this to work you will also need host keys in /etc/ssh/ssh_known_hosts\n#HostbasedAuthentication no\n# Change to yes if you don't trust ~/.ssh/known_hosts for\n# HostbasedAuthentication\n#IgnoreUserKnownHosts no\n# Don't read the user's ~/.rhosts and ~/.shosts files\n#IgnoreRhosts yes\n\n# To disable tunneled clear text passwords, change to no here!\nPasswordAuthentication no\nPermitEmptyPasswords no\n\n# Change to yes to enable challenge-response passwords (beware issues with\n# some PAM modules and threads)\nChallengeResponseAuthentication no\n\n# Kerberos options\n#KerberosAuthentication no\n#KerberosOrLocalPasswd yes\n#KerberosTicketCleanup yes\n#KerberosGetAFSToken no\n\n# GSSAPI options\n#GSSAPIAuthentication no\n#GSSAPICleanupCredentials yes\n#GSSAPIStrictAcceptorCheck yes\n#GSSAPIKeyExchange no\n\n# Set this to 'yes' to enable PAM authentication, account processing,\n# and session processing. If this is enabled, PAM authentication will\n# be allowed through the ChallengeResponseAuthentication and\n# PasswordAuthentication.  Depending on your PAM configuration,\n# PAM authentication via ChallengeResponseAuthentication may bypass\n# the setting of \"PermitRootLogin without-password\".\n# If you just want the PAM account and session checks to run without\n# PAM authentication, then enable this but set PasswordAuthentication\n# and ChallengeResponseAuthentication to 'no'.\nUsePAM yes\n\n#AllowAgentForwarding yes\n#AllowTcpForwarding yes\n#GatewayPorts no\nX11Forwarding yes\n#X11DisplayOffset 10\n#X11UseLocalhost yes\n#PermitTTY yes\nPrintMotd no\n#PrintLastLog yes\n#TCPKeepAlive yes\n#PermitUserEnvironment no\n#Compression delayed\n#ClientAliveInterval 0\n#ClientAliveCountMax 3\n#UseDNS no\n#PidFile /var/run/sshd.pid\n#MaxStartups 10:30:100\n#PermitTunnel no\n#ChrootDirectory none\n#VersionAddendum none\n\n# no default banner path\n#Banner none\n\n# Allow client to pass locale environment variables\nAcceptEnv LANG LC_*\n\n# override default of no subsystems\nSubsystem\tsftp\t/usr/lib/openssh/sftp-server\n\n# Example of overriding settings on a per-user basis\n#Match User anoncvs\n#\tX11Forwarding no\n#\tAllowTcpForwarding no\n#\tPermitTTY no\n#\tForceCommand cvs server\n```\n\n### 6. Prepare the System for my Application\n\n1. Install required system packages. \n2. Install snap.\n3. Update snap.\n4. Install Certbot.\n5. Check the status of newly installed services.\n\n ```bash\n sudo apt install git python3-pip python3-venv nginx gunicorn clamav snapd\n snap install core; snap refresh core\n snap install --classic certbot\n sudo systemctl status nginx\n sudo systemctl status fail2ban\n ```\n\nAt this point, my application is secure enough to start updating, upgrading, and installing packages. Debian 12 comes \npreinstalled with python3, openssh-server, ufw and other packages I may need in the future. \nUsing a virtual environment for my applications helps keep python packages local to my project and prevents conflicts. \nThis means I can have multiple sets of packages and modules for the same project which makes changing and testing much easier.\nThis is a quick overview of the packages I installed and why I need each one.\n\n- git. Version control system to manage the changes I make to my code.\n- python3-pip and python3-venv. Pythons package manager along a virtual development environment package handle installing and updating the tools that my application needs.\n- gunicorn and nginx. A wsgi along with a powerful web server and reverse proxy optimize my application for the Web and provide a protective layer between the sensitive information in my app and the Web.\n- clamav\n- Fail2Ban is a utility that will scan my log file and ban any IPs that show malicious activity such as too many password failures or seeking exploits.\n- Clamav an anti virus/malware service.\n- snapd and certbot. Linux's snap daemon. It manages the apps and dependencies that certbot need to work.\n\n## Application Setup\n\nThese sections go over the steps needed to build, test, and deploy my application. \n\n### 7. Prepare the Application\n\n1. Create an application directory.\n2. Change ownership of the html folder to my user. [^1]\n3. Set permissions the application directory.\n4. Initialize a git repository.\n5. Clone my project.\n6. Create a virtual environment.\n7. Activate the environment\n8. Install the required packages.\n9. Run the test suite and ensure everything works.\n10. Deactivate the virtual environment.\n\n     ```bash\n    sudo mkdir -p /var/www/my_project\n    sudo chown -R $USER:$USER /var/www/my_project\n    sudo chmod -R 755 /var/www/my_project\n    git init\n    git clone https://github.com/my-username/my-project.git\n    python3 -m venv my_project_venv\n    source my_project_venv/bin/\n    python3 -m pip install -r requirements.txt\n    python3 wsgi.py\n    gunicorn wsgi:test_wsgi -b 0.0.0.0:80\n    deactivate\n     ```\n\nIn this section, I add my application files to a directory I own and add my application files. \nI use a virtual environment and a requirements.txt to keep my installed packages separate from the global system ones.\nI test my app with python and then with gunicorn.\nThe environment is deactivated, so I can install global packages again.\n\nTesting here is important because applications that work flawlessly in a local environment are run through web server and web server gateway interfaces.\nThese tools are useful and expose problems that arise when using absolute paths and many other bugs I haven't encountered yet.\nAfter all bugs are fixed to documented I can move on to the next step.\n\nGunicorn recommends that you run it with the system package installed with apt rather than the python package?\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n### 8. Set up the Docker container\n\n1. C \n2. T \n\n    ```bash\n   ec\n   ec\n   ec\n    ```\n   \nDocker handles creating and activating the python virtual environment.\nI thhhhinkkk Docker runs the gunicorn instance inside a contained section of the server that only has the tools and applications that my application needs to run.\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n### 9. Set up the Web Server (NGINX)\n\n1. Stop any pre-installed web server service.\n2. Verify the nginx installation.\n3. Check the configuration file for errors. \n4. Stop and restart any nginx services then check the status of the service.\n\n    ```bash\n   sudo mkdir\n   sudo nginx -v\n   sudo nginx -t\n   sudo systemctl stop nginx\n   sudo systemctl start nginx\n   sudo systemctl status nginx\n    ```\n\nNginx is my production web server. \nIt stores, process, and delivers the information in my application to the web.  \n\n\n### 10. Domain Registration\n\nNow, I will connect my server to a domain name. This allows users to access my website without needing to type the \nentire IP address. I'll associate a hostname that corresponds to my server's IPv4 address with an 'A' record, and my \nsite's domain name to my server's IPv6 address with an 'AAAA' record.\n\n1. Sign in to your DNS provider's control panel and register a domain name.\nIf you don't have a DNS provider, some popular options include Namecheap, AWS route 53, and Google Domains.\n2. Create two ```A``` records:\nFor your root domain host, use @. \nFor your subdomain host, use www. \nFor both records, include your server's public IPv4 address.\n3. Create two AAAA records:\nUse the same details as for the ```A``` records, but include your server's public IPv6 address instead.\n4. Wait for the DNS changes to propagate.\nThis can take from a few minutes to up to 48 hours.\nAfter this, open your domain name in a web browser. \n5. If you see the Nginx welcome page, it means your DNS setup is correct and the web server is accessible through the \ndomain name.\n\n### 11. Configure Server Blocks for Each Website\n\n1. Create a server block for my domain and application to use.\n2. Enable the server block by creating a symbolic links between\n3. Prevent a possible hash bucket memory problem.\n4. Ensure there are no errors in any nginx files.\n5. Restart nginx to enable the changes.\n\n   ```bash\n   sudo echo '\n   server { \n        listen 80;\n        listen [::]:80;\n\n        root /var/www/my_project;\n        index index.html index.htm index.nginx-debian.html;\n\n        server_name my-domain.com www.my-domain.com;\n\n        location / {\n                try_files $uri $uri/ =404;\n        }\n   }'\n   \u003e /etc/nginx/sites-available/my_project\n   sudo ln -s /etc/nginx/sites-available/my_project /etc/nginx/sites-enabled/\n   sudo nano /etc/nginx/nginx.conf\n   sudo nginx -t\n   sudo systemctl restart nginx\n    ```\n\nNow I set up server blocks to allow my websites to be managed from one web server.\nThis lets me keep configuration details for each of my domains separate.\nThe chmod command changes permissions for the owner of a file or directory, all users in the same primary group as\nthe owner, and for everyone else included unauthenticated and anonymous users.\nWhen I set 600 for the .ssh folder, I allow the owner which is my user to read and write.\nOther users and unauthenticated users cannot read, write, or execute commands.\n\nChange this section to point to the Docker container running the gunicorn instance instead of a gunicorn instance. ???\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n### 12. Configure SSL Certificates\n\n1. Get the SSL certificate \n\n    ```bash\n   certbot --nginx -d mydomain.name\n    ```\n\nCertBot will secure our website with a free ssl cert. \nHaving a valid SSL cert allows the site to use HTTP an improved more secure protocol than HTTP. \nNginx comes with a plugin that handles reconfiguring and reloading the config when necessary.\n\n### 13. Configure Automated Deployment\n\n1. Create a systemd service file to automatically run applications on when Ubuntu starts.\n2. Start my service.\n3. Enable my service to start when the system powers on.\n4. Check the status of the service and make sure there are no errors.\n\n    ```bash\n   sudo echo \n   '\n   [Unit]\n   Description= Project Description\n   After=network.target\n\n   [Service]\n   User=my-username\n   Group=www-data\n   WorkingDirectory=/var/www/my_project\n   Environment=\"PATH=/var/www/my_project/project_venv/bin\"\n   ExecStart=/var/www/my_project/project_venv/bin/gunicorn --workers 3 --bind unix:project_name.sock -m 007 wsgi:app\n\n   [Install]\n   WantedBy=multi-user.target\n   '\n   \u003e /systemd/system/my_project.service\n   sudo systemctl start my_project.service\n   sudo systemctl enable my_project.service\n   sudo systemctl status my_project.service\n\n    ```\n\nNow that my application is ready I can set my server to run my app on when the system starts.\n\nChange this to start the docker container on system start.\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n## Conclusion\n\n1. SSH keys are used to securely access my public server.\n2. The system is updated to maintain its security.\n3. A user with sudo privileges is created for everyday tasks.\n4. A firewall is enabled to prevent unwanted or unnecessary network traffic \n5. All other SSH logins options are disabled.\n6. System packages the server needs to function are installed.\n7. My application is copied to the server and tested with Python and Gunicorn.\n8. The Docker container to store my application is created.\n9. Nginx is configured to serve my application files.\n10. My application is connected to a domain name, so I can access it more easily.\n11. A section of my web sever is designated to my test application specifically. \n12. An SSL certificate is generated in order to encrypt the data accessed by a users web browser.\n13. A systemd service is created to ??? start my Docker container whenever my system starts or restarts.\n\nIn conclusion, this guide outlines a comprehensive approach to deploying a Python application on a public server, \nusing best practices for security and maintainability; from setting up SSH keys and system updates to leveraging Docker, Nginx, and SSL encryption. \nOnce mastered, I plan to automate this workflow through a shell script and augment it with additional features such as SSH key rotation, monitoring, and backups for future deployments.\n\n## Notes \n\nunix permissions \n\n[^1]: The chmod command changes permissions for the owner of a file or directory, all users in the same primary group as\nthe owner, and for everyone else included unauthenticated and anonymous users.\nWhen I set 600 for the .ssh folder I allow the owner which is my user to read and write.\nOther users and unauthenticated users cannot read, write, or execute commands.\n\n## Sources \n\nSecuring Debian - https://www.debian.org/doc/manuals/securing-debian-manual/\nNginx Config Structure - https://www.digitalocean.com/community/tutorials/understanding-the-nginx-configuration-file-structure-and-configuration-contexts\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fruffinweb%2Fdevopsguide","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fruffinweb%2Fdevopsguide","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fruffinweb%2Fdevopsguide/lists"}