{"componentChunkName":"component---src-templates-blog-list-jsx","path":"/blog/11/","result":{"data":{"prismic":{"allFeaturedblogs":{"edges":[{"node":{"featured_blogs_enabled":true,"heading":[{"type":"paragraph","text":"Featured posts","spans":[]}],"featured_blog_1":{"__typename":"PRISMIC_Blog","_linkType":"Link.document","blog_header_image":{"dimensions":{"width":790,"height":395},"alt":null,"copyright":null,"url":"https://images.prismic.io/www-static/6d8d81b1-971a-4313-b033-b4e125cb14a0_MondoDB-blog-header-790x395.PNG?auto=compress,format"},"blog_headline":[{"type":"heading1","text":"Introducing DigitalOcean Managed MongoDB – a fully managed, database as a service for modern apps","spans":[]}],"blog_post_date":"2021-06-29","blog_post_content":[{"type":"paragraph","text":"MongoDB is one of the most popular databases, and it’s ideal for apps that evolve rapidly and need to handle huge volumes of data and traffic. It offers advantages like flexible document schemas, code-native data access, change-friendly design, and easy horizontal scale-out.","spans":[{"start":22,"end":44,"type":"hyperlink","data":{"link_type":"Web","url":"https://db-engines.com/en/ranking","target":"_blank"}}]},{"type":"paragraph","text":"However, building and maintaining MongoDB clusters from the ground up can be a huge undertaking. Developers often complain that they have to spend their valuable time and resources on database management. Well, we’ve been listening and have some great news: accessing and managing MongoDB on DigitalOcean just got a lot simpler!","spans":[]},{"type":"paragraph","text":"We are excited to announce that DigitalOcean Managed MongoDB is now in General Availability. Managed MongoDB is a fully managed, database as a service (DBaaS) offering from DigitalOcean, built in partnership with and certified by MongoDB Inc. It provides you all the technical capabilities that make MongoDB so beloved in the developer community. Together we have ensured that you will get access to all the latest releases of the MongoDB document database as they become available.","spans":[{"start":32,"end":91,"type":"hyperlink","data":{"link_type":"Web","url":"https://www.digitalocean.com/products/managed-databases-mongodb/"}},{"start":230,"end":241,"type":"hyperlink","data":{"link_type":"Web","url":"https://www.mongodb.com/","target":"_blank"}}]},{"type":"paragraph","text":"Managed MongoDB simplifies the MongoDB administration. Developers of all skill levels, even those who do not have prior experience in databases, can spin up MongoDB clusters in just a few minutes. We handle the provisioning, managing, scaling, updates, backups, and security of your MongoDB clusters, allowing you to offload the complex, time consuming –yet critical – database administration tasks to us. This empowers you to focus on what really matters: building awesome apps.","spans":[]},{"type":"embed","oembed":{"height":113,"width":200,"embed_url":"https://www.youtube.com/watch?v=NvHQSV7jnKA","type":"video","version":"1.0","title":"Create a MongoDB Database on DigitalOcean","author_name":"DigitalOcean","author_url":"https://www.youtube.com/c/Digitalocean","provider_name":"YouTube","provider_url":"https://www.youtube.com/","cache_age":null,"thumbnail_url":"https://i.ytimg.com/vi/NvHQSV7jnKA/hqdefault.jpg","thumbnail_width":480,"thumbnail_height":360,"html":"<iframe width=\"200\" height=\"113\" src=\"https://www.youtube.com/embed/NvHQSV7jnKA?feature=oembed\" frameborder=\"0\" allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen></iframe>"}},{"type":"heading2","text":"Benefits of Managed MongoDB","spans":[]},{"type":"paragraph","text":"","spans":[]},{"type":"list-item","text":"Easy set up and maintenance: We create the database clusters for you. Simply choose the cluster configuration (e.g., memory, disk size, number of nodes, etc.), and the data center in which you want to host the database. Follow a few simple steps and your database cluster will be up and running in a matter of minutes. You can spin up clusters using the cloud control panel, CLI, or API.\n\n","spans":[{"start":0,"end":28,"type":"strong"}]},{"type":"list-item","text":"Automatic daily backups with point in time recovery: Data is one of the most important assets of an app, so it’s critical to backup your database. We take backups of your entire clusters automatically on a daily basis, for free. We also provide a point in time recovery for 7 days, that way if things go wrong due to human error, machine error, or some combination of both, you can easily restore the database as it was at any point in the previous 7 days. \n\n","spans":[{"start":0,"end":52,"type":"strong"}]},{"type":"list-item","text":"Automatic updates and access to latest MongoDB releases: You get access to MongoDB 4.4. This is the latest release of MongoDB and comes packed with numerous enhancements like hedged reads, rust, and swift drivers. Since we have developed Managed MongoDB in partnership with MongoDB Inc, you will always get access to new releases as they become available. With Managed MongoDB, the updates happen automatically. Just select a date and time for the updates and we take care of the rest. This makes it easy to stay up to date with MongoDB releases without disrupting your business.\n\n","spans":[{"start":0,"end":56,"type":"strong"},{"start":148,"end":169,"type":"hyperlink","data":{"link_type":"Web","url":"https://www.mongodb.com/new","target":"_blank"}}]},{"type":"list-item","text":"High availability with automated failover: If your database goes down, it can take down the entire app, leading to bad customer experiences. With Managed MongoDB, you can easily minimize the downtime for your database and make it highly available with standby nodes. Standby nodes add redundancy, so if for example the primary node fails, the standby node is immediately promoted to primary and begins serving requests while we provision a replacement standby node in the background.\n\n","spans":[{"start":0,"end":42,"type":"strong"}]},{"type":"list-item","text":"Scale up easily to handle traffic spikes: As your app gains traction and the usage grows, it’s important to have a database that can keep up with the increased demand. With Managed MongoDB, you can easily scale up the size of database nodes when needed.\n\n","spans":[{"start":0,"end":41,"type":"strong"}]},{"type":"list-item","text":"Secure by default: Since data is critical, it also needs to be secure. We encrypt data at rest with LUKS and in transit with SSL. When you create a new cluster, it’s placed in a VPC network by default that provides a more secure connection between resources. You can also restrict access to your nodes to prevent brute-force password and denial-of-service attacks.","spans":[{"start":0,"end":18,"type":"strong"},{"start":178,"end":189,"type":"hyperlink","data":{"link_type":"Web","url":"https://www.digitalocean.com/docs/networking/vpc/"}}]},{"type":"heading2","text":"The need for Managed Databases","spans":[]},{"type":"paragraph","text":"DigitalOcean’s mission is to simplify cloud computing so developers, startups, and SMBs can spend more time building software that changes the world. While databases are a critical component to any application, building, maintaining, and scaling them can be complex and time consuming. For developers that are building apps for their business, database administration is often not a core focus area. But it’s quite common to find developers that write the code and then also roll up their sleeves to maintain databases. Such users would rather offload the tedious database administration and focus their limited time and energy on building and enhancing their apps. ","spans":[]},{"type":"paragraph","text":"With this in mind, we introduced Managed Databases a couple of years ago and are excited to add Managed MongoDB to our portfolio. With this release, DigitalOcean Managed Databases now supports the following engines:","spans":[{"start":33,"end":50,"type":"hyperlink","data":{"link_type":"Web","url":"https://www.digitalocean.com/products/managed-databases/"}}]},{"type":"image","url":"https://images.prismic.io/www-static/87745cc1-1c5f-4463-b104-104b7fc30dc7_managed-databases-logos.png?auto=compress,format","alt":null,"copyright":null,"dimensions":{"width":849,"height":104}},{"type":"paragraph","text":"Managed MongoDB launch comes on the heels of DigitalOcean App Platform, a modern, reimagined PaaS (Platform as a Service) that we released a few months ago. App Platform makes it very easy to build, deploy, and scale apps and static sites. You can deploy code by simply pointing to your GitHub and GitLab repos, and App Platform will do all the heavy lifting of managing infrastructure, app runtimes, and dependencies. App Platform, along with Managed Databases, helps fulfill DigitalOcean’s mission by empowering developers, startups, and SMBs to focus more on their apps, and less on the underlying infrastructure and databases.","spans":[{"start":45,"end":70,"type":"hyperlink","data":{"link_type":"Web","url":"https://www.digitalocean.com/products/app-platform/"}}]},{"type":"heading2","text":"How Managed MongoDB works","spans":[]},{"type":"paragraph","text":"DigitalOcean provides you with various compute options to build your apps like:","spans":[]},{"type":"list-item","text":"Droplets: On-demand, Linux virtual machines suitable for production business applications and personal passion projects.","spans":[{"start":0,"end":8,"type":"hyperlink","data":{"link_type":"Web","url":"https://www.digitalocean.com/products/droplets/"}}]},{"type":"list-item","text":"DigitalOcean Kubernetes: Managed Kubernetes with automatic scaling, upgrades, and a free control plane.","spans":[{"start":0,"end":23,"type":"hyperlink","data":{"link_type":"Web","url":"https://www.digitalocean.com/products/kubernetes/"}}]},{"type":"list-item","text":"DigitalOcean App Platform: A fully managed Platform as a Service.","spans":[{"start":0,"end":25,"type":"hyperlink","data":{"link_type":"Web","url":"https://www.digitalocean.com/products/app-platform/"}}]},{"type":"paragraph","text":"No matter which compute option you choose to build your apps, you can easily add Managed MongoDB to it. In addition to this, Managed MongoDB also integrates with the Node.js 1-Click App from DigitalOcean Marketplace making it a lot easier to build Node.js apps.","spans":[{"start":166,"end":215,"type":"hyperlink","data":{"link_type":"Web","url":"https://marketplace.digitalocean.com/apps/nodejs"}}]},{"type":"heading2","text":"Simple, predictable pricing","spans":[]},{"type":"paragraph","text":"Just like all DigitalOcean products, Managed MongoDB provides simple, predictable pricing that allows you to control costs and prevent any surprise bills. You can spin up a database cluster for just $15/month, or a highly available three-node replica set for $45/month. Click here for more information.","spans":[{"start":270,"end":301,"type":"hyperlink","data":{"link_type":"Web","url":"https://www.digitalocean.com/pricing/#managed-databases"}}]},{"type":"heading2","text":"Regional availability","spans":[]},{"type":"paragraph","text":"Managed MongoDB is currently available in the following regions:","spans":[]},{"type":"list-item","text":"NYC3 (New York, USA)","spans":[]},{"type":"list-item","text":"FRA1 (Frankfurt, Germany)","spans":[]},{"type":"list-item","text":"AMS3 (Amsterdam, Netherlands)","spans":[]},{"type":"paragraph","text":"We will be making Managed Mongo available in other regions soon. Please check out the release notes for most up to date information on regional availability.","spans":[{"start":86,"end":99,"type":"hyperlink","data":{"link_type":"Web","url":"https://www.digitalocean.com/docs/release-notes/"}}]},{"type":"heading2","text":"Join us at deploy, DigitalOcean’s virtual user conference","spans":[]},{"type":"paragraph","text":"Today we have deploy, DigitalOcean’s signature user conference, which focuses on celebrating, educating, and connecting awesome builders from all over the world.","spans":[{"start":14,"end":20,"type":"hyperlink","data":{"link_type":"Web","url":"https://deploy.digitalocean.com/home"}}]},{"type":"paragraph","text":"Check out the keynote session from DigitalOcean's CEO, Yancey Spruill, in which he talks about where we're headed as a company and shares some exciting product updates. His keynote will be followed by sessions from community members, engineers, customers, and other experts that are building technologies and businesses powered by the cloud. With live Q&A and an active Discord server, there’s ample opportunity to engage and learn something new. Click here to attend the deploy conference.","spans":[{"start":14,"end":69,"type":"hyperlink","data":{"link_type":"Web","url":"https://deploy.digitalocean.com/agenda/session/552806"}},{"start":347,"end":384,"type":"hyperlink","data":{"link_type":"Web","url":"http://do.co/deploy-discord"}},{"start":461,"end":489,"type":"hyperlink","data":{"link_type":"Web","url":"http://do.co/deploy"}}]},{"type":"paragraph","text":"We are also launching a hackathon for DigitalOcean Managed MongoDB. Learn how you can participate, submit an app and get a t-shirt.","spans":[{"start":24,"end":66,"type":"hyperlink","data":{"link_type":"Web","url":"https://www.digitalocean.com/mongodb-hackathon"}}]},{"type":"paragraph","text":"We hope you will give Managed MongoDB a try. Here are some sample datasets and sample apps that you can use to kick the tires. Check out the docs and let us know what you think!","spans":[{"start":22,"end":43,"type":"hyperlink","data":{"link_type":"Web","url":"https://cloud.digitalocean.com/databases/new?engine=mongodb"}},{"start":59,"end":90,"type":"hyperlink","data":{"link_type":"Web","url":"https://github.com/do-community/mongodb-resources","target":"_blank"}},{"start":141,"end":145,"type":"hyperlink","data":{"link_type":"Web","url":"https://docs.digitalocean.com/products/databases/mongodb/"}}]},{"type":"paragraph","text":"If you’d like to have a conversation about using DigitalOcean and Managed MongoDB in your business, please feel free to contact our sales team.","spans":[{"start":120,"end":142,"type":"hyperlink","data":{"link_type":"Web","url":"https://www.digitalocean.com/company/contact/sales/"}}]},{"type":"paragraph","text":"Happy coding!","spans":[]},{"type":"paragraph","text":"André Bearfield","spans":[]},{"type":"paragraph","text":"Director of Product Management","spans":[]}],"tags":[{"tag1":{"__typename":"PRISMIC_Tag","tag":"Product Updates","_linkType":"Link.document","_meta":{"uid":"product-updates"}}}],"author":{"__typename":"PRISMIC_Author","author_name":"André Bearfield","author_image":{"dimensions":{"width":553,"height":547},"alt":"André Bearfield","copyright":null,"url":"https://images.prismic.io/www-static/fdc7c85186f0a850b04083e1d4306bd1c19772e8_andre-bearfield.png?auto=compress,format"},"_meta":{"uid":"andre-bearfield"}},"_meta":{"uid":"introducing-digitalocean-managed-mongodb"}},"featured_blog_2":{"__typename":"PRISMIC_Blog","_linkType":"Link.document","blog_header_image":{"dimensions":{"width":790,"height":400},"alt":"Droplet Console","copyright":null,"url":"https://images.prismic.io/www-static/710499ae-78cc-4179-afc1-15793637b200_DODX3727-790x400-logo-2.jpg?auto=compress,format"},"blog_headline":[{"type":"heading1","text":"Securely connect to Droplets with SSH key pairs using a new Droplet Console","spans":[]}],"blog_post_date":"2021-08-10","blog_post_content":[{"type":"paragraph","text":"The famous author Ken Blanchard once said, “Feedback is the breakfast of champions.\" This is something we truly believe at DigitalOcean, and we always strive to enhance our products based on customer feedback.","spans":[]},{"type":"paragraph","text":"With this goal in mind, we are excited to introduce a new Droplet Console that will make it much easier to connect to your Droplets securely. The new Droplet Console provides one-click SSH access to your Droplets through a native-like SSH/Terminal experience. It also eliminates the need for a password or manual configuration of SSH keys. Starting today, we’re pleased to announce that the new Droplet Console is now available to all Droplet users.","spans":[]},{"type":"heading2","text":"Why you should be using Secure Shell (SSH) ","spans":[]},{"type":"paragraph","text":"Password-based security is notoriously insecure due to password fatigue and the overuse of passwords such as ‘123456’. Secure Shell or SSH is a network communication protocol that solves this by using passwordless solutions for encryption, enabling two computers to communicate and securely share data. At a high level, SSH works by creating cryptographic key pairs consisting of a public and private key, which are computer generated and stored separately to ensure their security. ","spans":[{"start":80,"end":117,"type":"hyperlink","data":{"link_type":"Web","url":"https://cybernews.com/best-password-managers/most-common-passwords/"}}]},{"type":"paragraph","text":"SSH has become the default encryption protocol for many industries, but it was difficult to use SSH keys with DigitalOcean’s current Recovery (VNC) console, which is why we developed our new Droplet Console. The new Droplet Console is backed by an agent that security supervises the key pair, while also providing one-click SSH access to our users. You can see the full list of features below.","spans":[]},{"type":"heading2","text":"The new Droplet Console: More time saving, less time wasting ","spans":[]},{"type":"paragraph","text":"The new Droplet Console is for everyone who is looking to build fast, secure apps and avoid hassles with SSH access & usability issues.","spans":[]},{"type":"paragraph","text":"In addition to easier SSH access, the new Droplet Console comes with:","spans":[]},{"type":"list-item","text":"Copy/paste text: Instead of typing lengthy key pairs and text manually, you can use copy/paste to save time. ","spans":[{"start":0,"end":17,"type":"strong"}]},{"type":"list-item","text":"Multi-color support: Multi-color support makes the console more useful and intuitive, and breaks the conventional standard appearance which is black text on a white background. ","spans":[{"start":0,"end":41,"type":"strong"}]},{"type":"list-item","text":"Multi-language support: DigitalOcean’s new Droplet Console supports multiple languages, meaning you can now type and view any content in any language that is supported by UTF-8","spans":[{"start":0,"end":24,"type":"strong"}]},{"type":"list-item","text":"OS/images supported: Linux distributions (Ubuntu(16.04 - 20.04), Fedora (32 & 33), Debian (9), CentOS (7.6 & 8.3), CentOS 8 Stream, Rocky Linux and Marketplace images.","spans":[{"start":0,"end":20,"type":"strong"},{"start":148,"end":159,"type":"hyperlink","data":{"link_type":"Web","url":"https://marketplace.digitalocean.com/"}}]},{"type":"paragraph","text":"The new Droplet Console is available by default on any new Droplets you spin up. You can also enable it manually on older Droplets. Click here to learn more!","spans":[{"start":132,"end":157,"type":"hyperlink","data":{"link_type":"Web","url":"https://docs.digitalocean.com/products/droplets/how-to/connect-with-console/"}}]},{"type":"paragraph","text":"Check out this short walkthrough video that shows the new Droplet Console in action: ","spans":[]},{"type":"embed","oembed":{"type":"video","embed_url":"https://www.youtube.com/watch?v=Qt7QihVuxiE","title":"Access Your Droplet Terminal Through the Web Console","provider_name":"YouTube","thumbnail_url":"https://i.ytimg.com/vi/Qt7QihVuxiE/hqdefault.jpg","provider_url":"https://www.youtube.com/","author_name":"DigitalOcean","author_url":"https://www.youtube.com/c/Digitalocean","height":113,"width":200,"version":"1.0","thumbnail_height":360,"thumbnail_width":480,"html":"<iframe width=\"200\" height=\"113\" src=\"https://www.youtube.com/embed/Qt7QihVuxiE?feature=oembed\" frameborder=\"0\" allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen></iframe>"}},{"type":"paragraph","text":"We hope you’re excited about the new Droplet Console. You’re welcome to spin some Droplets up right now, and try out the new Droplet Console – why wait?","spans":[{"start":72,"end":103,"type":"hyperlink","data":{"link_type":"Web","url":"https://cloud.digitalocean.com/droplets/new"}}]},{"type":"paragraph","text":"Happy coding!","spans":[]},{"type":"paragraph","text":"Harsh Banwait, Senior Product Manager","spans":[]}],"tags":[{"tag1":{"__typename":"PRISMIC_Tag","tag":"Product Updates","_linkType":"Link.document","_meta":{"uid":"product-updates"}}}],"author":{"__typename":"PRISMIC_Author","author_name":"Harsh Banwait","author_image":{"dimensions":{"width":600,"height":399},"alt":null,"copyright":null,"url":"https://images.prismic.io/www-static/e83ff690-b20c-4d88-a2b6-57e562558cd6_download.png?auto=compress,format"},"_meta":{"uid":"harsh-banwait"}},"_meta":{"uid":"new-droplet-console-ssh-support"}},"featured_blog_3":{"__typename":"PRISMIC_Blog","_linkType":"Link.document","blog_header_image":{"dimensions":{"width":790,"height":400},"alt":null,"copyright":null,"url":"https://images.prismic.io/www-static/588e28d3-d41e-480b-937b-8c3b19201f6e_DODX3568-790x400-Blog.jpg?auto=compress,format"},"blog_headline":[{"type":"heading1","text":"How to scale your SaaS product without breaking the bank","spans":[]}],"blog_post_date":"2021-06-22","blog_post_content":[{"type":"paragraph","text":"These days, if you are in the business of software, chances are you are delivering or plan to deliver your services using a Software-as-a-Service (SaaS) model. A combination of internet-based delivery, subscription-based pricing, and low-friction product experiences have made SaaS solutions valuable tools for their users, and an excellent vehicle for software builders looking to distribute their products.","spans":[]},{"type":"paragraph","text":"These factors have made SaaS solutions ubiquitous; SaaS is the largest segment in the public cloud market, and is used to provide functionality ranging from personal finance apps for consumers, to productivity software for businesses, and even tools and services for software developers themselves to compose their applications and simplify their workflows. It is also not uncommon to find micro-SaaS applications being built for specific industries such as retail, job functions such as accounting or marketing, or tasks such as event management. ","spans":[]},{"type":"paragraph","text":"The best thing about this SaaS wave has been that it has allowed a new generation of software builders to build and monetize applications and participate in the digital economy. Previously, you had to be a big company with lots of resources, name recognition and distribution networks to successfully sell software products. Now, irrespective of whether you are a single person working on a passion project, a small team of developers in a startup, or a small and medium-sized business (SMB), the SaaS model enables you to express your ideas in the form of software and deliver them to customers anywhere in the world.","spans":[]},{"type":"heading2","text":"The unique challenges of building SaaS solutions","spans":[]},{"type":"paragraph","text":"","spans":[]},{"type":"paragraph","text":"Despite the opportunities that come with the widespread adoption of SaaS products, software builders still have to answer key questions in their journey to building successful SaaS products. Understanding what customers to target, features to prioritize, how to price your product, and how to acquire customers are all critical questions to figure out while you are also doing the important job of actually building and operating the product. ","spans":[]},{"type":"paragraph","text":"Writing the code, testing, deployment, monitoring the usage in production, and ensuring that your apps are able to handle the additional demand when customer base and usage grows are all essential and time-consuming tasks.","spans":[]},{"type":"paragraph","text":"Additionally, being able to test multiple ideas, pivot, and double down on the ideas that actually work is critical in early stages of SaaS development. Once growth comes, it is equally important to scale up without compromising on performance or reliability. Needless to say, all of this needs to be economically viable as well, since not everyone has the resources of large SaaS providers like Salesforce or Adobe.","spans":[]},{"type":"heading2","text":"Cloud Computing enables builders but also poses challenges","spans":[]},{"type":"paragraph","text":"","spans":[]},{"type":"paragraph","text":"Fortunately, for the act of building and operating your apps, cloud computing can help take some load off your shoulders. Unless you have the scale and resources of Facebook, chances are you are not going to set up your own data centers to host the computing infrastructure that powers your SaaS company. Public cloud infrastructure providers can bring great value to SaaS builders by providing on-demand computing services with usage-based pricing. However, just like how the legacy software companies weren't built for the SaaS model, the early (and big) cloud computing services were not optimized for the unique needs of small SaaS building teams. ","spans":[]},{"type":"paragraph","text":"Smaller SaaS teams face challenges with large cloud computing providers, including:","spans":[]},{"type":"heading4","text":"Too many technology options","spans":[]},{"type":"paragraph","text":"There are just too many options for tech stacks on which to build your SaaS - programming languages, application development frameworks, libraries, runtime environments, architectural patterns, and deployment models - and the list is growing by the day.","spans":[]},{"type":"heading4","text":"Complexity of cloud computing services","spans":[]},{"type":"paragraph","text":"Even when you have decided on a technology stack, there is a lot of cloud vendor-specific terminology you need to learn and heavy lifting you need to do to build on the cloud, not all of which contributes to making your SaaS applications successful.","spans":[]},{"type":"heading4","text":"Unpredictable costs","spans":[]},{"type":"paragraph","text":"The experimentation necessary in early stages of SaaS development, as well as the scaling of applications required during the growth phase, call for affordable and predictable pricing from your cloud provider. The last thing SaaS teams want is surprising and indecipherable bills from your cloud provider. Unfortunately, smaller businesses often experience unpredictable costs with cloud providers who are busy serving only the large enterprises.","spans":[]},{"type":"heading2","text":"DigitalOcean provides a simple, cost effective solution for SaaS builders","spans":[]},{"type":"paragraph","text":"Fortunately, at DigitalOcean we have a laser focus on small software development teams, who are trying to build the next generation of applications. Today, DigitalOcean customers are already building SaaS applications which serve all kinds of customers.","spans":[{"start":191,"end":217,"type":"hyperlink","data":{"link_type":"Web","url":"https://www.digitalocean.com/solutions/saas/"}}]},{"type":"paragraph","text":"We believe SaaS builders should focus on building apps that power their business, and not spend their valuable time on managing infrastructure. That is exactly what we have been able to enable through our intuitive products that are built for scale and reliability.","spans":[{"start":205,"end":223,"type":"hyperlink","data":{"link_type":"Web","url":"https://www.digitalocean.com/products/"}}]},{"type":"list-item","text":"Vidazoo is an advertising technology company specializing in video streaming and serving. It serves video ads to thousands of websites and handles close to 10 billion requests per day. \n\n“We are as much a data company as an adtech company. Our business relies on speedy and accurate data processing at massive scale. DigitalOcean provides us the perfect set of tools to operate our SaaS business profitably, while not making us feel the need to become full time system administrators. We plan to move a lot of our apps to DigitalOcean App Platform and other fully managed products.” - Roman Svichar, CTO of Vidazoo","spans":[{"start":0,"end":7,"type":"hyperlink","data":{"link_type":"Web","url":"https://vidazoo.com/"}},{"start":187,"end":583,"type":"em"}]},{"type":"paragraph","text":"We believe in meeting customers where they are. If they already have an understanding of cloud infrastructure technologies, they should be able to leverage that knowledge and get started with our products without any further ramp up.","spans":[]},{"type":"list-item","text":"Whatfix is an enterprise SaaS provider that offers a digital adoption platform to businesses. The company helps enterprises gain the full value of their investments in enterprise applications by providing real-time, interactive, and contextual guidance to users of those applications. \n\n“What we really love about the DigitalOcean platform is the ease of use. We feel like we know infrastructure and can handle most of the configuration and management. What we needed from a cloud was not bells and whistles but efficiency and reliability. DigitalOcean provides us a platform to build our apps and then gets out of the way. Just how we like it.” - Achyuth Krishna, Director of Engineering of Whatfix","spans":[{"start":0,"end":7,"type":"hyperlink","data":{"link_type":"Web","url":"https://whatfix.com/blog/driving-the-future-now-were-excited-to-announce-our-90-million-series-d-funding/"}},{"start":287,"end":648,"type":"em"}]},{"type":"paragraph","text":"We understand that scaling while maintaining reliability of applications and profitability of business is important, so we provide robust solutions which minimize downtime.","spans":[]},{"type":"list-item","text":"Centra is a SaaS-based e-commerce platform for global direct-to-consumer and wholesale e-commerce brands. Centra provides a powerful e-commerce backend that lets brands build pixel-perfect, custom designed, online flagship stores. \n\n“How do we enable our customers to create differentiated online experiences? How do we ensure their e-commerce apps stay up and running at all times? How do we scale on-demand when traffic grows or new customers come in? These are the questions that we ask ourselves every day. Thankfully, we have a partner in DigitalOcean that provides just the platform to answer those questions enabling us to guarantee 99.9% uptime for our clients.” - Martin Jensen, CEO of Centra","spans":[{"start":0,"end":6,"type":"hyperlink","data":{"link_type":"Web","url":"https://centra.com/"}},{"start":233,"end":673,"type":"em"}]},{"type":"paragraph","text":"These are just a few examples of SaaS businesses finding success on DigitalOcean. We are constantly amazed by the creativity and innovation that software builders are utilizing our platform for. If you are interested in learning more about product updates, technical deep-dives and best practices for building SaaS products and businesses, please contact us to learn how we can help you get started. ","spans":[{"start":340,"end":357,"type":"hyperlink","data":{"link_type":"Web","url":"https://www.digitalocean.com/migrate/?utmmedium=blog","target":"_blank"}}]},{"type":"paragraph","text":"Come build with DigitalOcean!","spans":[]},{"type":"paragraph","text":"Looking to migrate your SaaS to DigitalOcean? Leverage free infrastructure credits, robust training, and technical support to ensure a worry-free migration.","spans":[{"start":0,"end":156,"type":"strong"},{"start":0,"end":156,"type":"hyperlink","data":{"link_type":"Web","url":"https://www.digitalocean.com/migrate/?utmmedium=blog","target":"_blank"}}]},{"type":"paragraph","text":"","spans":[]},{"type":"paragraph","text":"Raman Sharma","spans":[]},{"type":"paragraph","text":"Vice President, Product & Programs Marketing","spans":[]}],"tags":[{"tag1":{"__typename":"PRISMIC_Tag","tag":"Developer Relations","_linkType":"Link.document","_meta":{"uid":"developer-relations"}}}],"author":{"__typename":"PRISMIC_Author","author_name":"Raman Sharma","author_image":{"dimensions":{"width":512,"height":512},"alt":null,"copyright":null,"url":"https://images.prismic.io/www-static/497b4b14-d192-493a-8b66-7ae176ba99f3_raman.png?auto=compress,format"},"_meta":{"uid":"raman-sharma"}},"_meta":{"uid":"how-to-scale-your-saas-product-without-breaking-the-bank"}}}}]}}},"pageContext":{"limit":12,"skip":120,"numPages":33,"currentPage":11,"data":[{"node":{"author":{"_linkType":"Link.document","author_name":"Andrew Starr-Bochicchio","author_image":null,"_meta":{"uid":"asb"}},"blog_header_image":{"dimensions":{"width":1200,"height":600},"alt":null,"copyright":null,"url":"https://images.prismic.io/www-static/587ccdbe-88e0-4278-a9c0-8c18408e764a_image5.png?auto=compress,format"},"blog_headline":[{"type":"heading1","text":"How to Deploy to DigitalOcean Kubernetes with GitHub Actions","spans":[]}],"blog_post_content":[{"type":"heading4","text":"Update - September 9, 2019:","spans":[]},{"type":"paragraph","text":"The examples in this blog post use the HCL syntax used in the initial version of GitHub Actions. GitHub Actions v2 now uses a new YAML syntax. You can find an updated workflow using the new syntax in the in this example repository.","spans":[{"start":0,"end":231,"type":"strong"},{"start":97,"end":114,"type":"hyperlink","data":{"link_type":"Web","url":"https://github.blog/2019-08-08-github-actions-now-supports-ci-cd/"}},{"start":207,"end":230,"type":"hyperlink","data":{"link_type":"Web","url":"https://github.com/andrewsomething/example-doctl-action/blob/master/.github/workflows/workflow.yaml"}}]},{"type":"heading2","text":"Introduction","spans":[]},{"type":"paragraph","text":"GitHub Actions were one of the most exciting things launched by our friends at GitHub last year. Now that they're in public beta, people are using them to build awesome stuff, from running tests and linters to more lighthearted use cases. With the DigitalOcean doctl Action, you can interact with all of your DigitalOcean resources.","spans":[{"start":0,"end":14,"type":"hyperlink","data":{"link_type":"Web","url":"https://github.com/digitalocean/action-doctl"}},{"start":161,"end":174,"type":"hyperlink","data":{"link_type":"Web","url":"https://github.com/sdras/awesome-actions"}},{"start":215,"end":237,"type":"hyperlink","data":{"link_type":"Web","url":"https://github.com/jessfraz/shaking-finger-action"}},{"start":248,"end":273,"type":"hyperlink","data":{"link_type":"Web","url":"https://github.com/digitalocean/action-doctl"}}]},{"type":"paragraph","text":"One of the most powerful aspects of GitHub Actions is the ability to compose workflows using multiple Actions to accomplish complicated tasks. In this post, we’ll show what that looks like in practice.","spans":[]},{"type":"paragraph","text":"Using multiple Actions, including ones for DigitalOcean and Docker, we’ll build a simple continuous delivery pipeline that deploys an application to a DigitalOcean Kubernetes cluster on push to the master branch of a GitHub repository. Along the way, we’ll dig into some of the details of working with GitHub Actions.","spans":[{"start":151,"end":182,"type":"hyperlink","data":{"link_type":"Web","url":"https://www.digitalocean.com/products/kubernetes/"}}]},{"type":"heading2","text":"Creating Your Workflow","spans":[]},{"type":"image","url":"https://images.prismic.io/www-static/65f9f2b0-2464-4e5d-ac9e-b94693e8f464_image2.png?auto=compress,format","alt":null,"copyright":null,"dimensions":{"width":1849,"height":858}},{"type":"paragraph","text":"The first step in using GitHub Actions is to create a workflow. You can do this from the Actions tab of your GitHub repository. This is where you define what will trigger a run of your workflow. ","spans":[]},{"type":"paragraph","text":"Nearly any GitHub event can be used from a new PR being opened to a new release being tagged. In our example, we’ll be using the “push” event so that our workflow is executed when a new commit is pushed to the master branch.","spans":[{"start":0,"end":23,"type":"hyperlink","data":{"link_type":"Web","url":"https://developer.github.com/actions/managing-workflows/workflow-configuration-options/#events-supported-in-workflow-files"}}]},{"type":"image","url":"https://images.prismic.io/www-static/77c1578a-03c6-4b3e-9fe5-532c99fc0bcf_image1-1.png?auto=compress,format","alt":null,"copyright":null,"dimensions":{"width":289,"height":153}},{"type":"paragraph","text":"This will create a new file in your repository at .github/main.workflow with the following contents:","spans":[]},{"type":"preformatted","text":"```[php]{`","spans":[]},{"type":"paragraph","text":"    workflow \"New workflow\" {  ","spans":[]},{"type":"paragraph","text":"      on = \"push\"","spans":[]},{"type":"paragraph","text":"    }","spans":[]},{"type":"preformatted","text":"`}```","spans":[]},{"type":"paragraph","text":"This highlights an important aspect of GitHub Actions. While workflows can be created and edited using the GitHub GUI, they are configured in code using HCL – the same format used by tools like [HashiCorp’s Terraform](https://www.terraform.io/). Each change made in the GUI is mirrored in the file and will be committed to the repository. This allows you to edit your workflows offline and collaborate on them via pull requests. For the rest of this post, we’ll mostly be showing the examples as code so that it is easier to see the details of how all the pieces fit together.","spans":[]},{"type":"heading2","text":"Defining Your First Action","spans":[]},{"type":"paragraph","text":"Our repository contains a Dockerfile in its root directory that defines how to build and run our application. In order to keep our example simple and focused on the workflow rather than the details of the application, our “application” is just a static site served by NGINX. The first Action block that we’ll define will build a container image from this Dockerfile:","spans":[{"start":26,"end":36,"type":"hyperlink","data":{"link_type":"Web","url":"https://github.com/andrewsomething/example-doctl-action/blob/master/Dockerfile"}}]},{"type":"preformatted","text":"```[php]{`","spans":[]},{"type":"paragraph","text":"    action \"Build Docker image\" {  ","spans":[]},{"type":"paragraph","text":"      uses = \"actions/docker/cli@master\"","spans":[]},{"type":"paragraph","text":"      args = [\"build\", \"-t\", \"andrewsomething/static-example:$(echo $GITHUB_SHA | head -c7)\", \".\"]","spans":[]},{"type":"paragraph","text":"    }","spans":[]},{"type":"preformatted","text":"`}```","spans":[]},{"type":"paragraph","text":"The first line is just a label for the block; the interesting bits are inside. The ```[php]{`uses`}``` line specifies the Action that will be run. The path used to reference the Action matches its location on GitHub. For instance, here we are using the Docker CLI Action which can be found in the cli/ directory of the github.com/actions/docker repository. This Action is a wrapper around the same Docker CLI tool that you would use on the command line locally. ","spans":[{"start":319,"end":355,"type":"hyperlink","data":{"link_type":"Web","url":"https://github.com/actions/docker/tree/master/cli"}}]},{"type":"paragraph","text":"If you have ever built a Docker image, the next line should look familiar. The ```[php]{`args`}``` line is just what it sounds like. Here we can pass arguments to the Docker command needed to build the image.","spans":[]},{"type":"paragraph","text":"When we build the image, we are tagging it so that we can push it to Docker Hub. If you are following along, make sure to replace \"andrewsomething\" with your own username. You probably noticed that we are using the $GITHUB_SHA environment variable as part of the tag. Its value is the SHA of the commit that triggered the workflow. It is one of a number of variables made available in the Action’s runtime environment. ","spans":[{"start":347,"end":366,"type":"hyperlink","data":{"link_type":"Web","url":"https://developer.github.com/actions/creating-github-actions/accessing-the-runtime-environment/#environment-variables"}}]},{"type":"heading2","text":"Using Secrets","spans":[]},{"type":"paragraph","text":"Often you will need to store secrets that your Action will require in order to run. Our next Action block demonstrates this. To push the image we built to Docker Hub, we will first need to log in. Using the `secrets` line of an Action block, we can securely pass the needed information as environment variables:  ","spans":[]},{"type":"preformatted","text":"```[php]{`","spans":[]},{"type":"paragraph","text":"    action \"Docker Login\" {  ","spans":[]},{"type":"paragraph","text":"      uses = \"actions/docker/login@master\"","spans":[]},{"type":"paragraph","text":"      secrets = [\"DOCKER_USERNAME\", \"DOCKER_PASSWORD\"]","spans":[]},{"type":"paragraph","text":"    }","spans":[]},{"type":"preformatted","text":"`}```","spans":[]},{"type":"paragraph","text":"The contents of these secrets can be configured in the GitHub GUI:","spans":[]},{"type":"image","url":"https://images.prismic.io/www-static/7298f06d-eb92-40f5-b859-0cc15eb6d928_image3.png?auto=compress,format","alt":null,"copyright":null,"dimensions":{"width":340,"height":311}},{"type":"paragraph","text":"While we’re here, we will also specify a ```[php]{`DIGITALOCEAN_ACCESS_TOKEN`}``` secret using a personal access token generated from the API section of the DigitalOcean Control Panel. We’ll be using this in a later step.","spans":[{"start":138,"end":183,"type":"hyperlink","data":{"link_type":"Web","url":"https://www.digitalocean.com/docs/api/create-personal-access-token/"}}]},{"type":"heading2","text":"Specifying Dependencies","spans":[]},{"type":"paragraph","text":"In the next step of our workflow, we’ll push the Docker image to Docker Hub. This looks similar to the previous Action blocks, but this time we have a new line:","spans":[]},{"type":"preformatted","text":"```[php]{`","spans":[]},{"type":"paragraph","text":"    action \"Push image to Docker Hub\" {  ","spans":[]},{"type":"paragraph","text":"      needs = [\"Docker Login\", \"Build Docker image\"]","spans":[]},{"type":"paragraph","text":"      uses = \"actions/docker/cli@master\"","spans":[]},{"type":"paragraph","text":"      args = [\"push\", \"andrewsomething/static-example\"]","spans":[]},{"type":"paragraph","text":"    }","spans":[]},{"type":"preformatted","text":"`}```","spans":[]},{"type":"paragraph","text":"Multiple Action blocks may run in parallel. In this case, we need to ensure that the Docker image has been built and that we have logged into Docker Hub before we can push it there. So we’ve specified a `needs` line referencing the labels for those two Action blocks so that they will be executed in the correct order.","spans":[]},{"type":"heading2","text":"Accessing Your Workspace","spans":[]},{"type":"paragraph","text":"The config directory of our repository contains a Kubernetes YAML file specifying our deployment. As committed in git, there is only a placeholder for the Docker image that we want to deploy. It will need to be updated to point to the image we’ve tagged and pushed to Docker Hub. To do this, we’ll use the Shell Action provided by GitHub. Based on Debian, it includes all the standard UNIX tools you’d expect. Here we’re using ```[php]{`sed`}``` to update the contents of our deployment file:","spans":[{"start":61,"end":96,"type":"hyperlink","data":{"link_type":"Web","url":"https://github.com/andrewsomething/example-doctl-action/blob/master/config/deployment.yml"}},{"start":306,"end":318,"type":"hyperlink","data":{"link_type":"Web","url":"https://github.com/actions/bin/tree/master/sh"}}]},{"type":"preformatted","text":"```[php]{`","spans":[]},{"type":"paragraph","text":"    action \"Update deployment file\" {  ","spans":[]},{"type":"paragraph","text":"      needs = [\"Push image to Docker Hub\"]","spans":[]},{"type":"paragraph","text":"      uses = \"actions/bin/sh@master\"","spans":[]},{"type":"paragraph","text":"      args = [\"TAG=$(echo $GITHUB_SHA | head -c7) && sed -i 's|<IMAGE>|andrewsomething/static-example:'${TAG}'|' $GITHUB_WORKSPACE/config/deployment.yml\"]","spans":[]},{"type":"paragraph","text":"    }","spans":[]},{"type":"preformatted","text":"`}```","spans":[]},{"type":"paragraph","text":"This demonstrates another important environment variable available to you, ```[php]{`$GITHUB_WORKSPACE`}```. This directory contains a copy of the repository that triggered the workflow. Changes made here will persist from one step to the next.","spans":[]},{"type":"heading2","text":"Deploying to DigitalOcean Kubernetes","spans":[]},{"type":"paragraph","text":"In our next step, we’ll retrieve the credentials needed to access our Kuberenetes cluster using the DigitalOcean doctl Action. This Action enables you to use any doctl sub-command just like from the command line giving you access to all of your DigitalOcean resources. Using the ```[php]{`DIGITALOCEAN_ACCESS_TOKEN`}``` secret we configured earlier, we will save the kubeconfig file for our cluster:","spans":[{"start":113,"end":125,"type":"hyperlink","data":{"link_type":"Web","url":"https://github.com/digitalocean/action-doctl"}}]},{"type":"preformatted","text":"```[php]{`","spans":[]},{"type":"paragraph","text":"    action \"Save DigitalOcean kubeconfig\" {  ","spans":[]},{"type":"paragraph","text":"      uses = \"digitalocean/action-doctl@master\"","spans":[]},{"type":"paragraph","text":"      secrets = [\"DIGITALOCEAN_ACCESS_TOKEN\"]","spans":[]},{"type":"paragraph","text":"      args = [\"kubernetes cluster kubeconfig show actions-example > $HOME/.kubeconfig\"]","spans":[]},{"type":"paragraph","text":"    }","spans":[]},{"type":"preformatted","text":"`}```","spans":[]},{"type":"paragraph","text":"Next, we’ll configure an Action block using ```[php]{`kubectl`}``` to apply the actual deployment:","spans":[]},{"type":"preformatted","text":"```[php]{`","spans":[]},{"type":"paragraph","text":"    action \"Deploy to DigitalOcean Kubernetes\" {  ","spans":[]},{"type":"paragraph","text":"      needs = [\"Save DigitalOcean kubeconfig\", \"Update deployment file\"]","spans":[]},{"type":"paragraph","text":"      uses = \"docker://lachlanevenson/k8s-kubectl\"","spans":[]},{"type":"paragraph","text":"      runs = \"sh -l -c\"","spans":[]},{"type":"paragraph","text":"      args = [\"kubectl --kubeconfig=$HOME/.kubeconfig apply -f $GITHUB_WORKSPACE/config/deployment.yml\"]","spans":[]},{"type":"paragraph","text":"    }","spans":[]},{"type":"preformatted","text":"`}```","spans":[]},{"type":"paragraph","text":"You’ll notice something new in this block demonstrating just how flexible GitHub Actions can be. In this case, the `uses` line is not specifying an Action on GitHub like our previous steps. Instead, it is referencing a container image hosted on DockerHub. This opens up a whole world of tools not packaged as Actions for use in your workflow.","spans":[{"start":205,"end":234,"type":"hyperlink","data":{"link_type":"Web","url":"https://developer.github.com/actions/managing-workflows/workflow-configuration-options/#using-a-dockerfile-image-in-an-action"}}]},{"type":"heading2","text":"Verifying the Deployment","spans":[]},{"type":"paragraph","text":"In the final step of our workflow, using the same kubectl Docker image, we will check on the status of our deployment. The kubectl rollout status command returns a zero exit code when a deployment was successful:","spans":[{"start":123,"end":153,"type":"hyperlink","data":{"link_type":"Web","url":"https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#complete-deployment"}}]},{"type":"preformatted","text":"```[php]{`","spans":[]},{"type":"paragraph","text":"    action \"Verify deployment\" {  ","spans":[]},{"type":"paragraph","text":"      needs = [\"Deploy to DigitalOcean Kubernetes\"]","spans":[]},{"type":"paragraph","text":"      uses = \"docker://lachlanevenson/k8s-kubectl\"","spans":[]},{"type":"paragraph","text":"      runs = \"sh -l -c\"","spans":[]},{"type":"paragraph","text":"      args = [\"kubectl --kubeconfig=$HOME/.kubeconfig rollout status deployment/static-example\"]","spans":[]},{"type":"paragraph","text":"    }","spans":[]},{"type":"preformatted","text":"`}```","spans":[]},{"type":"paragraph","text":"If the deployment fails, it returns a non-zero exit code. So that the status of our workflow will correctly reflect whether or not our application was successfully deployed, we will return to our workflow block from the first step and add a new ```[php]{`resolves`}``` line:","spans":[]},{"type":"preformatted","text":"```[php]{`","spans":[]},{"type":"paragraph","text":"    workflow \"New workflow\" {  ","spans":[]},{"type":"paragraph","text":"      on = \"push\"","spans":[]},{"type":"paragraph","text":"      resolves = [\"Verify deployment\"]","spans":[]},{"type":"paragraph","text":"    }","spans":[]},{"type":"preformatted","text":"`}```","spans":[]},{"type":"paragraph","text":"Since our \"Verify deployment\" Action depends on all of our other Actions, we can specify it here alone. If our workflow contained completely independent Actions, we’d want to include each of them here.","spans":[]},{"type":"heading2","text":"Bringing It All Together","spans":[]},{"type":"paragraph","text":"Now that we’ve successfully configured our workflow, each time a commit is pushed to the master branch of our repository it will be triggered. Each step will run in the order that we specified. The GitHub GUI will display the progress:","spans":[]},{"type":"image","url":"https://images.prismic.io/www-static/ca1f3803-92dc-4ba1-9f54-e2d2ce1202d1_image4.png?auto=compress,format","alt":null,"copyright":null,"dimensions":{"width":701,"height":898}},{"type":"paragraph","text":"With everything green, our site is now live: https://doctl-action.do-api.dev/","spans":[{"start":45,"end":77,"type":"hyperlink","data":{"link_type":"Web","url":"https://doctl-action.do-api.dev/"}}]},{"type":"paragraph","text":"You can find the complete workflow file with the full end-to-end example on GitHub.","spans":[{"start":49,"end":72,"type":"hyperlink","data":{"link_type":"Web","url":"https://github.com/andrewsomething/example-doctl-action/blob/master/.github/main.workflow"}}]},{"type":"heading2","text":"Next Steps","spans":[]},{"type":"paragraph","text":"GitHub Actions allow you to craft powerful workflows integrating multiple Actions to accomplish complicated tasks. In this post, we’ve only scratched the surface of what they can do. With the doctl Action, you can incorporate your DigitalOcean resources into your workflows. Here are a few resources to help you get started building your own:","spans":[]},{"type":"list-item","text":"Check out the source for the DigitalOcean doctl Action on GitHub.","spans":[{"start":29,"end":54,"type":"hyperlink","data":{"link_type":"Web","url":"https://github.com/digitalocean/action-doctl"}}]},{"type":"list-item","text":"Dig into Actions in more detail with GitHub's Actions Documentation.","spans":[{"start":37,"end":67,"type":"hyperlink","data":{"link_type":"Web","url":"https://developer.github.com/actions/managing-workflows/"}}]},{"type":"list-item","text":"Run your GitHub Actions locally with act. Great for debugging!","spans":[{"start":0,"end":41,"type":"hyperlink","data":{"link_type":"Web","url":"https://github.com/nektos/act"}}]},{"type":"paragraph","text":"In this post we mostly focused on the GitHub Actions side of the equation. If you’re looking for more info on working with Kubernetes, the DigitalOcean Kubernetes Resource Center is a great place to start.","spans":[{"start":139,"end":178,"type":"hyperlink","data":{"link_type":"Web","url":"https://www.digitalocean.com/resources/kubernetes/"}}]},{"type":"paragraph","text":"We’d love to know how you are using GitHub Actions. So let us know in the comments below! Are there other Actions for DigitalOcean that you’d like to see? Share your feedback and requests by opening an issue on GitHub.","spans":[{"start":191,"end":217,"type":"hyperlink","data":{"link_type":"Web","url":"https://github.com/digitalocean/action-doctl/issues"}}]}],"blog_post_date":"2019-04-24","tags":[{"tag1":{"tag":"Engineering","_linkType":"Link.document","_meta":{"uid":"engineering"}}}],"_meta":{"uid":"how-to-deploy-to-digitalocean-kubernetes-with-github-actions"}}},{"node":{"author":{"_linkType":"Link.document","author_name":"DigitalOcean","author_image":{"dimensions":{"width":600,"height":600},"alt":"Sammy avatar","copyright":null,"url":"https://images.prismic.io/www-static/a10e3c2eb15b74ee43f872be3044313423b1c9a9_sammy_avatar.png?auto=compress,format"},"_meta":{"uid":"digitalocean"}},"blog_header_image":{"dimensions":{"width":800,"height":600},"alt":"computer illustration","copyright":null,"url":"https://images.prismic.io/www-static/2406e46c6320ad83e8d9f275de03872e03b759ff_jarvis-company-of-one.png?auto=compress,format"},"blog_headline":[{"type":"heading1","text":"How to 10X Your Web & WordPress Agency","spans":[]}],"blog_post_content":[{"type":"paragraph","text":"This is a guest post from Lukas Hertig, SVP of Business Development at Plesk.","spans":[{"start":0,"end":77,"type":"em"},{"start":71,"end":76,"type":"hyperlink","data":{"link_type":"Web","url":"https://www.plesk.com/"}}]},{"type":"paragraph","text":"Many agency owners struggle with burnout and profitability, mostly because of one key issue: they offer services on an automated time and material model. These agencies scale only by delivering more hours to customers, making growth cumbersome.","spans":[]},{"type":"paragraph","text":"It doesn’t have to be this way. In this post, you'll explore how to improve your web or WordPress agency's business model.","spans":[]},{"type":"heading2","text":"Leverage the 10X Rule in your Agency","spans":[]},{"type":"paragraph","text":"A cardinal rule in sales is to think big – 10X – for whatever you do in life and business. If you run an agency and are struggling to make it profitable, you can apply this rule to be more successful.","spans":[]},{"type":"paragraph","text":"As an agency owner and entrepreneur, you might want to use this thinking to:","spans":[]},{"type":"list-item","text":"10X the goals you are trying to achieve","spans":[]},{"type":"list-item","text":"10X the number of your clients","spans":[]},{"type":"list-item","text":"10X your income and revenue, especially recurring revenue","spans":[]},{"type":"list-item","text":"Save 10X of your time by automating 10X of your processes","spans":[]},{"type":"list-item","text":"Sleep 10X better because your client websites run on a secure and performant environment","spans":[]},{"type":"heading2","text":"Offer Recurring Value Instead of Time-based Services","spans":[]},{"type":"paragraph","text":"A modern option to help scale your agency by 10X is to implement as many services as possible as recurring services, in a value-based model instead of a time- and material-based model. In this model, you don’t sell your time; you sell results or outcomes.","spans":[]},{"type":"paragraph","text":"Tools such as Plesk and DigitalOcean's agency program have made it easy for smaller agencies to compete against larger firms by offering recurring value.","spans":[{"start":14,"end":19,"type":"hyperlink","data":{"link_type":"Web","url":"https://marketplace.digitalocean.com/apps/plesk"}},{"start":24,"end":53,"type":"hyperlink","data":{"link_type":"Web","url":"https://www.digitalocean.com/partners/agency-partners/"}}]},{"type":"paragraph","text":"Plesk manages your websites and WordPress instances in a graphical web interface. It’s everything you need to build, secure, and run websites and applications to simplify your life as a web professional.","spans":[]},{"type":"paragraph","text":"Here’s how this might work in practice:","spans":[]},{"type":"paragraph","text":"An auto retailer pays $5 per month for hosting their static website created by a website builder tool years ago. Their website is not optimized for performance, and they are interested in rebuilding it with WordPress and connecting it to a modern technical marketing stack with the goal of selling more cars.","spans":[]},{"type":"paragraph","text":"A traditional pricing model may look something like this: \nActivityCostWebsite development100 hours at $60/hour = $6,000Website content migration50 hours at $60/hour = $3,000Website hosting (shared, non-performant)$5 per monthWebsite monitoring & maintenance2 hours per month at $60/hour = $120 per monthSEO3 hours per month at $60/hour = $180 per monthSocial media marketing (inc ads)8 hours per month at $60/hour = $480 per monthTotal one-time costs$9,000Total monthly costs$785 per month\nLet’s compare that to a customer-centric, value-based approach with the same scope of work.","spans":[{"start":431,"end":451,"type":"strong"},{"start":451,"end":457,"type":"strong"},{"start":457,"end":476,"type":"strong"},{"start":476,"end":490,"type":"strong"}]},{"type":"paragraph","text":"You might begin by asking your customer what the averages sales value is per unit (in this case, a car). Let’s say it’s $25,000. At this price, the retailer may make an average margin of $5,000 per car.","spans":[]},{"type":"paragraph","text":"Therefore, if a website generates three additional sales per month for a customer, that would mean $900,000 additional revenue or $180,000 additional profit annually.","spans":[]},{"type":"paragraph","text":"Let’s say you charge 3% of this additional revenue as a one-time fee and 5% of it for recurring fees, per year. In return, you guarantee a minimum of three additional sales every month.","spans":[]},{"type":"paragraph","text":"Here's what the numbers look like:\nActivityPriceDevelopment fee (3% of AGR)$27,000Annual support fee (5% of AGR)$45,000Total one-time payment$27,000Total monthly payment$3,750 per month\nIncluded with support:","spans":[{"start":119,"end":141,"type":"strong"},{"start":141,"end":148,"type":"strong"},{"start":148,"end":169,"type":"strong"},{"start":169,"end":185,"type":"strong"}]},{"type":"list-item","text":"SEO monitoring and optimization","spans":[]},{"type":"list-item","text":"Ad creation and A/B testing","spans":[]},{"type":"list-item","text":"Social media engagement and management","spans":[]},{"type":"list-item","text":"Monthly reporting on KPIs","spans":[]},{"type":"paragraph","text":"Your one-time revenue just jumped by $18,000, and your recurring revenue increases by $2,965 every month!","spans":[]},{"type":"paragraph","text":"But that's not all:","spans":[]},{"type":"list-item","text":"The customer is happy – they get a guaranteed positive result, and they only spend more when they make more","spans":[]},{"type":"list-item","text":"You are happy – you get more revenue and can automate work, preserving more profit","spans":[]},{"type":"list-item","text":"Most costs for hosting and managing the website do not matter. You can charge whatever you like. Results are what count.","spans":[]},{"type":"paragraph","text":"Results may vary depending on industry and category, but there is almost always a way to create better offerings and maintain a more profitable business using recurring revenue.","spans":[]},{"type":"paragraph","text":"In these situations, your responsibility is to help the customer understand this as a necessary investment and not a cost. You need to demonstrate why your agency is the right team for the work. By focusing on results, you show that you understand the business as the customer does. You are not thinking about hours, as everyone else does.","spans":[]},{"type":"paragraph","text":"It’s crucial that you structure your offering thoughtfully. Here are some basic guidelines:","spans":[]},{"type":"list-item","text":"Target a certain niche with your services. Small and medium-sized businesses offer an armada of sub-niches you could target.","spans":[{"start":0,"end":42,"type":"strong"}]},{"type":"list-item","text":"Include relevant and proper maintenance and support services into your offering that the big agencies can’t.","spans":[{"start":0,"end":108,"type":"strong"}]},{"type":"list-item","text":"Never ever mention hours, days, or other time elements. Remember – you are providing value, not requesting a reimbursement for costs.\nSell your offerings by showing your customer how much value you provide.","spans":[{"start":0,"end":55,"type":"strong"}]},{"type":"list-item","text":"Combine hosting services with other metrics-based services such as social media marketing, content production, SEO, or paid advertising management. When combined with KPIs, reporting, maintenance and ongoing support, you’re delivering extraordinary value to your clients and creating loyal partnerships that will last for years to come.","spans":[{"start":0,"end":147,"type":"strong"}]},{"type":"paragraph","text":"Happy 10X’ing :-)","spans":[]},{"type":"paragraph","text":"Lukas is a born-in-the-cloud international senior business leader, investor, and speaker with 18+ years in IT – including 15+ years in the global cloud and hosting/IT service provider industry. He specializes in scaling tech startups, cloud, web hosting, WordPress, blockchain, entrepreneurship, investing and digital transformation.","spans":[{"start":0,"end":333,"type":"em"}]}],"blog_post_date":"2019-04-17","tags":[{"tag1":{"tag":"Marketplace","_linkType":"Link.document","_meta":{"uid":"marketplace"}}}],"_meta":{"uid":"how-to-10x-your-web-wordpress-agency"}}},{"node":{"author":{"_linkType":"Link.document","author_name":"Shiven Ramji","author_image":{"dimensions":{"width":170,"height":170},"alt":"Shiven Ramji","copyright":null,"url":"https://images.prismic.io/www-static/79c5726c75adb45644613d2371026b1bb789a415_shiven_ramji-090ac31e.png?auto=compress,format"},"_meta":{"uid":"shiven_ramji"}},"blog_header_image":{"dimensions":{"width":784,"height":418},"alt":"Layers of squares stacked illustration","copyright":null,"url":"https://images.prismic.io/www-static/e871afc1b88fe3c6b61f3da89493fa40490f5f67_nautilus_announce_blog_alt.jpg?auto=compress,format"},"blog_headline":[{"type":"heading1","text":"Nanobox Joins the DigitalOcean Family","spans":[]}],"blog_post_content":[{"type":"paragraph","text":"I am thrilled to share the exciting news today that we have acquired Nanobox, an application deployment and developer workflow automation platform. This acquisition brings both the passionate Nanobox team and their technology to DigitalOcean and propels us closer to our mission of bringing modern application development into reach for developers around the world – by automating DevOps workflows.","spans":[{"start":69,"end":76,"type":"hyperlink","data":{"link_type":"Web","url":"https://nanobox.io/"}}]},{"type":"paragraph","text":"Developers have consistently asked for simpler app development tools to speed time to deployment. The Nanobox technology and developer experience allow us to provide the services and experiences that help eliminate the burden of managing infrastructure for you and your team, so you can continue to focus on what’s important for your business: creating modern apps. Tyler, the founding CEO of Nanobox, and the Nanobox team share our vision and values, and they are equally excited to join us in this journey.","spans":[]},{"type":"paragraph","text":"While we are not ready to share specific product plans, I did want to share the news with our community and welcome the Nanobox team to the DigitalOcean family. We have a lot of work ahead of us in shaping our next set of offerings with this new capability. If you’d like to keep up with the developments, please sign up for updates. If you’d like to share ideas and problems you’d like us to solve with this technology, please leave a comment below. Our team is always eager to listen to the feedback from our customers and the developer community.","spans":[{"start":306,"end":332,"type":"hyperlink","data":{"link_type":"Web","url":"https://www.digitalocean.com/nanobox"}}]},{"type":"paragraph","text":"Happy coding,","spans":[]},{"type":"paragraph","text":"Shiv, SVP Product","spans":[]}],"blog_post_date":"2019-04-09","tags":[{"tag1":{"tag":"News","_linkType":"Link.document","_meta":{"uid":"news"}}}],"_meta":{"uid":"digitalocean-acquires-nanobox"}}},{"node":{"author":{"_linkType":"Link.document","author_name":"Priya Chakravarthi","author_image":{"dimensions":{"width":200,"height":200},"alt":"Priya Chakravarthi","copyright":null,"url":"https://images.prismic.io/www-static/a764a7c4d900d2e77bbd3a25ad5b2a348063df40_image.png?auto=compress,format"},"_meta":{"uid":"priya_chakravarthi"}},"blog_header_image":{"dimensions":{"width":1024,"height":512},"alt":"Spaces line illustration","copyright":null,"url":"https://images.prismic.io/www-static/ea8e73d7978e7e5b210fc9af4175a650af15e91c_spaces_digitalocean_blog.jpg?auto=compress,format"},"blog_headline":[{"type":"heading1","text":"Introducing Custom Subdomains for Spaces CDN","spans":[]}],"blog_post_content":[{"type":"paragraph","text":"At DigitalOcean, we are always working on making the cloud easier to use so that you can focus on building great things.","spans":[]},{"type":"paragraph","text":"Part of the journey in creating the developer cloud included launching Spaces, S3-compatible object storage that makes hosting web assets painless.  In September 2018, we launched the built-in CDN feature for Spaces that provided the ability to turn on global edge caching for a Space in any of the regions Spaces was available. This allowed our customers to deliver assets to their websites and applications up to 70% faster.","spans":[{"start":184,"end":215,"type":"hyperlink","data":{"link_type":"Web","url":"https://blog.digitalocean.com/spaces-now-includes-cdn/"}}]},{"type":"paragraph","text":"Today, we are pleased to announce custom subdomains for your Spaces CDN endpoints. Now customers can use their own subdomain URL to deliver assets from an S3-compatible Space and secure it with a TLS certificate.  These certificates can be issued by a CA authority, self-signed, or auto-generated through DigitalOcean’s Let’s Encrypt integration at no additional cost.","spans":[]},{"type":"image","url":"https://images.prismic.io/www-static/975c9f4ea8f936f2c83078d145ffe605789d132c_image-from-ios.png?auto=compress,format","alt":null,"copyright":null,"dimensions":{"width":1200,"height":530}},{"type":"heading1","text":"How Does it Work?","spans":[]},{"type":"paragraph","text":"If you don’t have a domain already, you need to purchase one from a domain name registrar. Then you need to set up DNS records for your domain by using a DNS hosting service. Please note that if you plan on using DigitalOcean’s Let’s Encrypt integration to generate TLS certificates for your CDN subdomain, you need to use DigitalOcean’s DNS hosting service. For self-signed certificates, you can use any DNS hosting service.  For a detailed walkthrough, see our product documentation on custom subdomains.","spans":[{"start":463,"end":505,"type":"hyperlink","data":{"link_type":"Web","url":"https://www.digitalocean.com/docs/spaces/how-to/customize-cdn-endpoint"}}]},{"type":"paragraph","text":"Add a DNS CNAME record, such as “assets,” that maps to the CDN endpoint of your Space, then assign your subdomain from the Settings menu for a pre-existing Space or when you enable CDN for a new Space.","spans":[]},{"type":"image","url":"https://images.prismic.io/www-static/655be4e5531da0520ed349abb5c342337ff1a5e7_swatch?auto=compress,format","alt":null,"copyright":null,"dimensions":{"width":100,"height":56}},{"type":"heading1","text":"Why Use Custom Subdomains?","spans":[]},{"type":"paragraph","text":"There are several reasons that custom subdomains could be useful for you.","spans":[]},{"type":"heading4","text":"Branding","spans":[]},{"type":"paragraph","text":"Agencies and web developers who use Spaces object storage to host their assets can now use their own subdomain.  Not only will this avoid confusion with end users, but it also keeps your tech stack hidden. Nobody but you needs to know where you host your digital assets.","spans":[]},{"type":"heading4","text":"Integrated SSL/TLS management","spans":[]},{"type":"paragraph","text":"Securing your Space is flexible and easy.  You can now upload a certificate for your own subdomain or use a free Let’s Encrypt cert that is simple to provision in just a few clicks, right from the Control Panel. The Let’s Encrypt certificate generation flow follows the same simple design principles used to generate certificates for DigitalOcean Load Balancers.","spans":[{"start":334,"end":361,"type":"hyperlink","data":{"link_type":"Web","url":"https://www.digitalocean.com/products/load-balancer/"}}]},{"type":"heading4","text":"Simple configuration","spans":[]},{"type":"paragraph","text":"Spaces users don’t need to spend extra engineering effort implementing a reverse proxy solution to hand off calls between *.cdn.digitaloceanspaces.com and *.myapp.com.  In addition, when mapping your own subdomain to the CDN endpoint, it gets automatically added to the CORS listings, saving you a step.  Now you can focus on building great applications rather than fine-tuning infrastructure.","spans":[]},{"type":"heading4","text":"Zero cost","spans":[]},{"type":"paragraph","text":"Custom subdomains and integrated TLS management are included with your Space at no additional cost.","spans":[]},{"type":"heading1","text":"What’s Next","spans":[]},{"type":"paragraph","text":"Web asset hosting is only one use case that can leverage a subdomain connected to a CDN endpoint. Customers have also been asking to host static websites from their Spaces as well. This feature is currently under development and will serve as a building block for a front-end-as-a-service solution that leverages some of the core building blocks of the object storage and CDN  infrastructure that customers have come to love.","spans":[]},{"type":"paragraph","text":"Get started and create a Space today.","spans":[{"start":0,"end":37,"type":"hyperlink","data":{"link_type":"Web","url":"https://cloud.digitalocean.com/spaces"}}]},{"type":"paragraph","text":"Happy Coding,","spans":[]},{"type":"paragraph","text":"Priya Chakravarthi,","spans":[]},{"type":"paragraph","text":"Product Manager","spans":[]}],"blog_post_date":"2019-04-09","tags":[{"tag1":{"tag":"Product Updates","_linkType":"Link.document","_meta":{"uid":"product-updates"}}}],"_meta":{"uid":"custom-subdomains-for-spaces-cdn-endpoints"}}},{"node":{"author":{"_linkType":"Link.document","author_name":"Karan Chhina","author_image":{"dimensions":{"width":554,"height":550},"alt":"Karan Chhina","copyright":null,"url":"https://images.prismic.io/www-static/b43a85223ca42f817f454615a86497acf668d7c3_karan.png?auto=compress,format"},"_meta":{"uid":"karan_chhina"}},"blog_header_image":{"dimensions":{"width":1200,"height":600},"alt":null,"copyright":null,"url":"https://images.prismic.io/www-static/0026a57b93abe5b04413765253903472dab58e11_general-droplets_blog-v4_twitter---facebook.png?auto=compress,format"},"blog_headline":[{"type":"heading1","text":"With General Purpose Droplets, you can DO more than ever","spans":[]}],"blog_post_content":[{"type":"paragraph","text":"When DigitalOcean launched back in 2012, our first product – the Droplet – gave developers quick access to virtual machines running in DigitalOcean’s sole NYC-based data center. Unlike other cloud providers that charged north of $90 per month, we kept monthly costs as low as $5 by sharing CPU threads among customers.","spans":[]},{"type":"paragraph","text":"Fast forward to 2019, and millions of developers rely on DigitalOcean to learn best practices and run apps in twelve data centers across seven countries. As we’ve grown, developers and business customers have asked us to provide the same simple Droplet developer experience, but for VMs with dedicated compute power.","spans":[]},{"type":"paragraph","text":"That’s why we’re so pleased that – as of today – our new General Purpose Droplets, with a 4:1 ratio of RAM to dedicated CPU, are now Generally Available. Our new Droplets are live and ready to deploy in five of our global data centers: in New York, San Francisco, Amsterdam, Frankfurt, and Singapore. With our new General Purpose Droplets, you’ll be able to do more than ever before.","spans":[{"start":133,"end":152,"type":"hyperlink","data":{"link_type":"Web","url":"https://www.digitalocean.com/docs/platform/product-lifecycle/#general-availability"}}]},{"type":"heading1","text":"Powering fast performance for production-grade applications","spans":[]},{"type":"paragraph","text":"General Purpose Droplets are backed by Intel Xeon Platinum 8168 \"Skylake\" Processors, which boast speeds of 2.7GHz. Because your vCPU has a dedicated physical CPU hardware thread, General Purpose Droplets guarantee uninterrupted high performance. Pricing is straightforward and highly competitive with our entry-level 8GB RAM/2 vCPU General Purpose Droplet costing $60 per month, or just $0.089 per hour.","spans":[{"start":39,"end":84,"type":"hyperlink","data":{"link_type":"Web","url":"https://ark.intel.com/content/www/us/en/ark/products/120504/intel-xeon-platinum-8168-processor-33m-cache-2-70-ghz.html"}},{"start":247,"end":254,"type":"hyperlink","data":{"link_type":"Web","url":"https://www.digitalocean.com/pricing/"}}]},{"type":"paragraph","text":"We ran a few micro-benchmarks to compare the performance of General Purpose Droplets with Standard Droplets (both instances have 8 vCPUs and 32 GB of RAM). The first case is Linpack, a high-performance computing benchmark that uses a large in-memory dataset to perform matrix multiplications. We ran the test 10 times, destroyed the instance, and recreated another one (which made the test run on a large number of different physical machines). The box plot below shows the range – the minimum, first quartile, median, third quartile, and maximum time – that it took to complete each run.","spans":[]},{"type":"image","url":"https://images.prismic.io/www-static/105ce33fc5cdb819a1a866a6ec255dd8185c4dbe_digitalocean-general-purpose-benchmark-linpack.png?auto=compress,format","alt":null,"copyright":null,"dimensions":{"width":2400,"height":2010}},{"type":"paragraph","text":"The second test case is the Terasort benchmark that uses Hadoop for map/reduce computations. In this configuration, we created one controller node and five worker nodes, all of which have to communicate to sort a large dataset. Similarly, we ran the test 10 times, then destroyed the instances, and recreated the same environment.","spans":[]},{"type":"image","url":"https://images.prismic.io/www-static/9eccad300df6d864dfde4c0475e396250dea5e9f_digitalocean-general-purpose-benchmark-terasort.png?auto=compress,format","alt":null,"copyright":null,"dimensions":{"width":2400,"height":2010}},{"type":"paragraph","text":"In both cases, the General Purpose Droplets’ dedicated compute power resulted in substantially shorter completion times and a more consistent performance profile. This makes them ideal for high-performance applications that require fast, predictable results.","spans":[]},{"type":"heading1","text":"Choosing the right Droplet for your app","spans":[]},{"type":"paragraph","text":"Many developers and operators prefer to first deploy their applications on VMs that possess the balance of RAM and CPU provided by General Purpose Droplets. After deploying with General Purpose Droplets, you can migrate to a different type of Droplet after performance testing. If you find that your app needs consistent compute power, but does not quite need the memory that our General Purpose Droplets provide, then CPU-Optimized Droplets may be a better option. On the other hand, if your app’s CPU usage is bursty but still requires balanced RAM, less expensive Standard Droplets may be a suitable choice.","spans":[{"start":212,"end":250,"type":"hyperlink","data":{"link_type":"Web","url":"https://www.digitalocean.com/docs/images/snapshots/how-to/migrate-droplets/"}}]},{"type":"paragraph","text":"But, for most production applications that demand consistent, fast performance for end users, General Purpose Droplets are probably your best bet.","spans":[{"start":0,"end":146,"type":"em"}]},{"type":"paragraph","text":"Your mileage may vary, but we typically recommend General Purpose Droplets for highly trafficked web and application servers, databases, ad servers, gaming servers, caching fleets, and other time-sensitive use cases.","spans":[]},{"type":"heading1","text":"A delightful developer experience, with add-ons to power your whole app","spans":[]},{"type":"paragraph","text":"General Purpose Droplets are tightly integrated into DigitalOcean and automatically inherit many qualities and features intended to make you productive and happy.","spans":[]},{"type":"paragraph","text":"Arguably, what’s long set DigitalOcean apart from other cloud platforms is the developer experience. While other clouds may feel uninspired and complicated, our platform and General Purpose Droplets are fun and simple. Now – when you create a Droplet within our Control Panel – you can simply select “General Purpose” for your Droplet’s plan. Then, with a simple click of a big green button, you can spin up your General Purpose Droplets in 55 seconds or less.","spans":[]},{"type":"image","url":"https://images.prismic.io/www-static/NWQ2NWExMTktYTNkZC00MmNmLWE4NmYtYjlkYmJkNmY1ZTE2_gen-purpose-droplet-tutorial-prod-demo.gif?auto=compress,format","alt":null,"copyright":null,"dimensions":{"width":600,"height":377}},{"type":"paragraph","text":"All Droplet types – General Purpose, Standard, and CPU Optimized – include features such as blazing-fast SSD storage, optional Backups and Snapshots, monitoring and alerts, network firewalls, and more.","spans":[]},{"type":"heading1","text":"Some customers have already gotten started. Now you can, too.","spans":[]},{"type":"paragraph","text":"Since we launched General Purpose Droplets in Limited Availability just over a month ago, many of our customers have taken them for a spin. One example is Prattle – a startup in St. Louis, Missouri, that provides sentiment data to predict the market impact of central bank and corporate communications. Here’s what their CTO, Bill MacMillan, had to say:","spans":[{"start":6,"end":66,"type":"hyperlink","data":{"link_type":"Web","url":"https://blog.digitalocean.com/introducing-general-purpose-droplets-dedicated-vcpus-and-more-memory/"}},{"start":155,"end":162,"type":"hyperlink","data":{"link_type":"Web","url":"https://prattle.co/"}}]},{"type":"preformatted","text":"Prattle has been working to move our computational burden into a highly distributed, multi-cloud system to maximize availability and dependability. After creating our DigitalOcean account, we spun up hundreds of General Purpose Droplets, basically within minutes. It was almost shocking how easy it was to run thousands of cores against our production workload. Our experience with other vendors hasn’t been nearly as simple and straightforward. DigitalOcean's launch of General Purpose Droplets has led me to rethink just where we should be putting our resources.\"","spans":[]},{"type":"paragraph","text":"In the months and years ahead, we’ll continue investing in infrastructure, including additional Droplet types and capacity, so that you can build your applications and your business on DigitalOcean. Early-stage startups should also check out Hatch, our program that helps entrepreneurs launch and scale their businesses in the cloud through learning and sharing via an engaged community of founders, prioritized support, and a year of free infrastructure credits.","spans":[{"start":232,"end":247,"type":"hyperlink","data":{"link_type":"Web","url":"https://www.digitalocean.com/hatch/"}}]},{"type":"paragraph","text":"We can’t wait to see what you do with General Purpose Droplets.","spans":[]}],"blog_post_date":"2019-04-02","tags":[{"tag1":{"tag":"Product Updates","_linkType":"Link.document","_meta":{"uid":"product-updates"}}},{"tag1":{"tag":"News","_linkType":"Link.document","_meta":{"uid":"news"}}}],"_meta":{"uid":"general-purpose-droplets-let-you-do-more"}}},{"node":{"author":{"_linkType":"Link.document","author_name":"DigitalOcean","author_image":{"dimensions":{"width":600,"height":600},"alt":"Sammy avatar","copyright":null,"url":"https://images.prismic.io/www-static/a10e3c2eb15b74ee43f872be3044313423b1c9a9_sammy_avatar.png?auto=compress,format"},"_meta":{"uid":"digitalocean"}},"blog_header_image":{"dimensions":{"width":800,"height":600},"alt":"computer illustration","copyright":null,"url":"https://images.prismic.io/www-static/2406e46c6320ad83e8d9f275de03872e03b759ff_jarvis-company-of-one.png?auto=compress,format"},"blog_headline":[{"type":"heading1","text":"Catch Paul Jarvis's AMA on Why Intentional Growth Matters","spans":[]}],"blog_post_content":[{"type":"paragraph","text":"Writer and entrepreneur Paul Jarvis has advised professional athletes like Steve Nash and Shaquille O’Neal, corporate giants like Microsoft and Mercedes-Benz, and entrepreneurs like Danielle LaPorte, Marie Forleo, and Kris Carr. His latest book, Company of One, explores why a bigger digital business isn’t always better, and he recently shared his knowledge with our DigitalOcean community in an AMA.","spans":[{"start":246,"end":260,"type":"hyperlink","data":{"link_type":"Web","url":"ofone.co"}}]},{"type":"paragraph","text":"He spoke on the importance of “intentional growth,” being that not all growth is good, and should simply be questioned before proceeding. Growing intentionally means, as founders, that we get to determine the type of business we want to run, how we define enough, and what goals should matter.","spans":[]},{"type":"paragraph","text":"You can download the slides from the short talk here or watch the AMA replay here.","spans":[{"start":8,"end":52,"type":"hyperlink","data":{"link_type":"Web","url":"https://marketing.nyc3.cdn.digitaloceanspaces.com/Product-Emails/Marketplace/PaulJarvis-IntentionalGrowth.pdf"}},{"start":56,"end":81,"type":"hyperlink","data":{"link_type":"Web","url":"https://youtu.be/XidEqur_Eyw"}}]},{"type":"paragraph","text":"","spans":[{"start":0,"end":0,"type":"hyperlink","data":{"link_type":"Web","url":"https://youtu.be/XidEqur_Eyw"}}]},{"type":"image","url":"https://images.prismic.io/www-static/0a3b765f68cb1420651075391a7ca4b8f368a51c_pauljarvis.png?auto=compress,format","alt":null,"copyright":null,"dimensions":{"width":1036,"height":484}}],"blog_post_date":"2019-03-25","tags":[{"tag1":{"tag":"Marketplace","_linkType":"Link.document","_meta":{"uid":"marketplace"}}}],"_meta":{"uid":"paul-jarvis-ama"}}},{"node":{"author":{"_linkType":"Link.document","author_name":"Tyler Crandall","author_image":{"dimensions":{"width":280,"height":280},"alt":"Tyler Crandall","copyright":null,"url":"https://images.prismic.io/www-static/445258e6ef5412ec1d759c61296620e393cea199_tyler_crandall-bd42a38f.png?auto=compress,format"},"_meta":{"uid":"tyler_crandall"}},"blog_header_image":{"dimensions":{"width":1568,"height":836},"alt":null,"copyright":null,"url":"https://images.prismic.io/www-static/b4ee6dba-f513-488e-98d7-4fbb7a0fb67d_LBaaS-1.5-Imagery-Blog-Header%402x_alt.jpg?auto=compress,format"},"blog_headline":[{"type":"heading1","text":"DigitalOcean Load Balancers Now Support Proxy Protocol","spans":[]}],"blog_post_content":[{"type":"paragraph","text":"DigitalOcean Load Balancers are a compelling, cost-efficient way to distribute traffic across backend servers, thanks to features such as automatic provisioning and renewal of SSL certificates, at a cost of just $10 per month (billed hourly at $0.015). Perhaps you're already among the thousands of developers who rely on DigitalOcean Load Balancers every day.","spans":[{"start":0,"end":27,"type":"hyperlink","data":{"link_type":"Web","url":"https://www.digitalocean.com/products/load-balancer/"}}]},{"type":"paragraph","text":"But while load balancers are great, they introduce a change that may matter in certain use cases: instead of your backend servers seeing the original client requests, backend servers see requests as though they had originated from load balancers. This means that, by default, backend servers no longer receive client information such as IP address and port number. The loss of this information is a problem if, for example, you want to analyze traffic logs, or to adjust your application’s functionality based on GeoIP.","spans":[]},{"type":"image","url":"https://images.prismic.io/www-static/a6ee6839-8653-42e0-b8d2-b99e863cf400_Proxy-Protol-Diagram_Proxy-Protocol-D1.png?auto=compress,format","alt":null,"copyright":null,"dimensions":{"width":1042,"height":626}},{"type":"paragraph","text":"To address this issue, today we’re enhancing DigitalOcean Load Balancers to support Proxy Protocol.","spans":[{"start":84,"end":98,"type":"hyperlink","data":{"link_type":"Web","url":"https://www.haproxy.com/blog/haproxy/proxy-protocol/"}}]},{"type":"heading3","text":"What is Proxy Protocol?","spans":[]},{"type":"paragraph","text":"Proxy Protocol is an industry standard to pass client connection information through a load balancer on to the destination server. DigitalOcean Load Balancers implement Proxy Protocol version 1, which simply prepends a human-readable header containing client information to the data sent to your Droplet.","spans":[]},{"type":"paragraph","text":"Turning on Proxy Protocol inserts a string formatted like this at the top of the request transmitted by the Load Balancer:","spans":[]},{"type":"paragraph","text":"```[php]{`PROXY_STRING + single space + INET_PROTOCOL + single space + CLIENT_IP +``single space + PROXY_IP + single space + CLIENT_PORT + single space + PROXY_PORT + \"\\r\\n\"`}```","spans":[]},{"type":"paragraph","text":"For example, a Proxy Protocol line for an IPv4 address would look like this:","spans":[]},{"type":"paragraph","text":"```[php]{`PROXY TCP4 192.168.0.1 192.168.0.2 42300 443\\r\\n`}```","spans":[]},{"type":"image","url":"https://images.prismic.io/www-static/d997161d-72dc-4949-bb0f-c9e1960d799e_Proxy-Protol-Diagram_Proxy-Protocol-D1-copy.png?auto=compress,format","alt":null,"copyright":null,"dimensions":{"width":1042,"height":626}},{"type":"heading3","text":"Turning on Proxy Protocol for Your Load Balancers","spans":[]},{"type":"paragraph","text":"All DigitalOcean Load Balancers now have the ability to turn on Proxy Protocol, at no additional cost. When you create a new Load Balancer, or when managing an existing one, you can activate Proxy Protocol by checking a box in the “Advanced settings” section.","spans":[]},{"type":"image","url":"https://images.prismic.io/www-static/c18ad232-42ba-4a69-a377-e4982e4b3fec_image.png?auto=compress,format","alt":null,"copyright":null,"dimensions":{"width":1001,"height":847}},{"type":"paragraph","text":"If you’re automating management of your infrastructure, you can also toggle the Proxy Protocol setting via our Load Balancer API.","spans":[{"start":111,"end":128,"type":"hyperlink","data":{"link_type":"Web","url":"https://developers.digitalocean.com/documentation/v2/#load-balancers"}}]},{"type":"paragraph","text":"Before turning on Proxy Protocol on your Load Balancers, make sure to configure your backend servers to accept Proxy Protocol. For example, here’s how to configure NGINX. If your backend servers are not configured for Proxy Protocol, the requests will fail.","spans":[{"start":154,"end":169,"type":"hyperlink","data":{"link_type":"Web","url":"https://docs.nginx.com/nginx/admin-guide/load-balancer/using-proxy-protocol/"}}]},{"type":"heading3","text":"Using DigitalOcean Kubernetes with Load Balancers and Proxy Protocol","spans":[]},{"type":"paragraph","text":"DigitalOcean Kubernetes (DOKS) is our new service for running the de facto standard container orchestration platform atop of Droplets. DigitalOcean Kubernetes seamlessly integrates with DigitalOcean Load Balancers so that you can provision Load Balancers simply by declaring them in a cluster’s resource configuration file.","spans":[{"start":0,"end":23,"type":"hyperlink","data":{"link_type":"Web","url":"https://www.digitalocean.com/products/kubernetes/"}}]},{"type":"paragraph","text":"With today’s launch of Proxy Protocol, the [DigitalOcean cloud controller manager has been updated to allow for creating Load Balancers of this type. Now you can ensure that each pod in your Kubernetes cluster can retrieve the original client IP address.","spans":[{"start":44,"end":81,"type":"hyperlink","data":{"link_type":"Web","url":"https://github.com/digitalocean/digitalocean-cloud-controller-manager"}}]},{"type":"paragraph","text":"DOKS clusters prior to version 1.11.9 need to contact support to have their master recycled prior to enabling proxy protocol. Clusters later than 1.11.9 have this functionality already enabled by default. Here's an example of how an annotation in the service manifest can be used to enable Proxy Protocol support.","spans":[{"start":233,"end":243,"type":"hyperlink","data":{"link_type":"Web","url":"https://www.digitalocean.com/docs/kubernetes/how-to/configure-load-balancers/#proxy-protocol"}}]},{"type":"heading3","text":"Get Started with DigitalOcean Load Balancers and Proxy Protocol Today","spans":[]},{"type":"paragraph","text":"DigitalOcean Load Balancers with Proxy Protocol are available in all regions for just $10 per month. For more information about Load Balancers, please check out these community tutorials:","spans":[{"start":0,"end":47,"type":"hyperlink","data":{"link_type":"Web","url":"https://www.digitalocean.com/products/load-balancer/"}}]},{"type":"list-item","text":"An Introduction to DigitalOcean Load Balancers","spans":[{"start":0,"end":46,"type":"hyperlink","data":{"link_type":"Web","url":"https://www.digitalocean.com/community/tutorials/an-introduction-to-digitalocean-load-balancers"}}]},{"type":"list-item","text":"How to Use Let’s Encrypt with DigitalOcean Load Balancers","spans":[{"start":0,"end":57,"type":"hyperlink","data":{"link_type":"Web","url":"https://www.digitalocean.com/community/tutorials/how-to-use-let-s-encrypt-with-digitalocean-load-balancers"}}]},{"type":"list-item","text":"Best Practices for Performance on DigitalOcean Load Balancers","spans":[{"start":0,"end":61,"type":"hyperlink","data":{"link_type":"Web","url":"https://www.digitalocean.com/community/tutorials/best-practices-for-performance-on-digitalocean-load-balancers"}}]},{"type":"paragraph","text":"Happy coding, ","spans":[]},{"type":"paragraph","text":"Tyler Crandall ","spans":[]},{"type":"paragraph","text":"Product Manager","spans":[]}],"blog_post_date":"2019-03-19","tags":[{"tag1":{"tag":"Product Updates","_linkType":"Link.document","_meta":{"uid":"product-updates"}}}],"_meta":{"uid":"load-balancers-now-support-proxy-protocol"}}},{"node":{"author":{"_linkType":"Link.document","author_name":"Kamal Nasser","author_image":{"dimensions":{"width":1008,"height":1008},"alt":null,"copyright":null,"url":"https://images.prismic.io/www-static/e2285fcfaf32ce7ec26329fe7e416ae896fbf991_portrait_2k18_bw_smallres.jpg?auto=compress,format"},"_meta":{"uid":"kamal-nasser"}},"blog_header_image":{"dimensions":{"width":1200,"height":900},"alt":null,"copyright":null,"url":"https://images.prismic.io/www-static/c4446d83-1e27-4c4e-822c-5ca308967e52_database-mostov_dribbble.png?auto=compress,format"},"blog_headline":[{"type":"heading1","text":"Creating a Simple Contacts List with Go and PostgreSQL","spans":[]}],"blog_post_content":[{"type":"paragraph","text":"In this post, we will build a simple web page containing a contacts list, with the contacts fetched from a PostgreSQL database. We will connect to the database in Go and use PostgreSQL's support for JSON columns. This is what the result will look like:","spans":[]},{"type":"image","url":"https://images.prismic.io/www-static/76f9193e-d599-4c79-b1d0-015d71bce6fb_Kamal-1.png?auto=compress,format","alt":null,"copyright":null,"dimensions":{"width":1600,"height":1396}},{"type":"paragraph","text":"By following this post, you will learn how to connect to a PostgreSQL database in Go using the sqlx and pgx packages, render data dynamically using a template, and serve the resulting page on an HTTP server.","spans":[]},{"type":"heading1","text":"Requirements","spans":[]},{"type":"paragraph","text":"Before we get started:","spans":[]},{"type":"o-list-item","text":"Make sure you have Go installed. See this post for instructions.  ","spans":[{"start":37,"end":46,"type":"hyperlink","data":{"link_type":"Web","url":"https://www.callicoder.com/golang-installation-setup-gopath-workspace/"}}]},{"type":"o-list-item","text":"Make sure you know where your GOPATH is. It's usually ~/go unless set differently.","spans":[]},{"type":"heading1","text":"Getting an HTTP Server Up","spans":[]},{"type":"paragraph","text":"In a new empty directory inside your $GOPATH, create a file named main.go. You can name the directory anything you like: I went with go-contacts. We'll start with setting up the HTTP server using Go's built-in net/http package.","spans":[]},{"type":"preformatted","text":"    package main\n    import (\n        \"flag\"\n        \"log\"\n        \"net/http\"\n        \"os\"\n    )\n    var (\n        listenAddr = flag.String(\"addr\", getenvWithDefault(\"LISTENADDR\", \":8080\"), \"HTTP address to listen on\")\n    )\n    func getenvWithDefault(name, defaultValue string) string {\n            val := os.Getenv(name)\n            if val == \"\" {\n                    val = defaultValue\n            }\n            return val\n    }\n    func main() {\n        flag.Parse()\n        log.Printf(\"listening on %s\\n\", *listenAddr)\n        http.ListenAndServe(*listenAddr, nil)\n    }","spans":[]},{"type":"paragraph","text":"The server will want a host and a port to listen on, so we ask for that in a CLI flag named addr. We also want to offer the option to pass in the setting in an environment variable, so the default value for the flag will be taken from the LISTENADDR environment variable. This means that if the CLI flag is passed, the value of the environment variable will be used. If neither are set, we'll fall back to port 8080.","spans":[]},{"type":"paragraph","text":"If you save the file and run it now, you should be able to browse to http://localhost:8080.","spans":[{"start":69,"end":90,"type":"hyperlink","data":{"link_type":"Web","url":"http://localhost:8080"}}]},{"type":"preformatted","text":"go run main.go","spans":[]},{"type":"paragraph","text":"and see—hold on, is that a \"404 page not found\" error?!","spans":[]},{"type":"paragraph","text":"That's fine! It's because we haven't configured any routes or pages yet, so the server doesn't know how to respond to the request. Why don't we go ahead and do that now.","spans":[]},{"type":"heading2","text":"Contacts List Page","spans":[]},{"type":"paragraph","text":"Let's create the contacts list page and serve it on the root path, /. We'll use the template/html package so that we can easily pass in dynamic data (the contacts) to be rendered in the page later.","spans":[]},{"type":"paragraph","text":"Create a directory named templates alongside main.go and within it a file named index.html with the following content:","spans":[]},{"type":"preformatted","text":"    <!doctype html>\n    <html>\n        <head>\n            <meta charset=\"utf-8\">\n            <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n            <title>Contacts</title>\n            <link rel=\"stylesheet\" href=\"https://unpkg.com/tachyons@4.10.0/css/tachyons.min.css\"/>\n        </head>\n        <body>\n            <div class=\"mw6 center pa3 sans-serif\">\n                <h1 class=\"mb4\">Contacts</h1>\n            </div>\n        </body>\n    </html>","spans":[]},{"type":"paragraph","text":"This is a page with basic styling that will serve as the base for our contacts list.","spans":[]},{"type":"paragraph","text":"Now we need to read the index.html template in our program. Import html/template and add a global variable to hold the templates right after listenAddr at the top:","spans":[]},{"type":"preformatted","text":"    import (\n        \"flag\"\n        \"log\"\n        \"html/template\"\n        \"net/http\"\n    )\n    var (\n            listenAddr       = flag.String(\"addr\", getenvWithDefault(\"LISTENADDR\", \":8080\"), \"HTTP address to listen on\")\n            tmpl             = template.New(\"\")\n    )","spans":[]},{"type":"paragraph","text":"Inside main(), after the flag.Parse() line, add the following. For compatibility with all operating systems, import the path/filepath package as we will use to construct the path to the template files.","spans":[]},{"type":"preformatted","text":"    var err error\n    _, err = tmpl.ParseGlob(filepath.Join(\".\", \"templates\", \"*.html\"))\n    if err != nil {\n        log.Fatalf(\"Unable to parse templates: %v\\n\", err)\n    }","spans":[]},{"type":"paragraph","text":"This will read every HTML file in the templates directory and prepare it for rendering. Now that we've done that, we want to configure the template to be rendered on /. Add a new function at the very bottom of the file to serve the page:","spans":[]},{"type":"preformatted","text":"    func handler(w http.ResponseWriter, r *http.Request) {\n        tmpl.ExecuteTemplate(w, \"index.html\", nil)\n    }","spans":[]},{"type":"paragraph","text":"Finally, configure the server to use this handler function. Above the log.Printf() line in main(), add:","spans":[]},{"type":"preformatted","text":"http.HandleFunc(\"/\", handler)","spans":[]},{"type":"paragraph","text":"Now we're ready! The whole file should look like this:","spans":[]},{"type":"preformatted","text":"    package main\n\n    import (\n        \"flag\"\n        \"log\"\n        \"html/template\"\n        \"net/http\"\n    )\n    var (\n        listenAddr = flag.String(\"addr\", getenvWithDefault(\"LISTENADDR\", \":8080\"), \"HTTP address to listen on\")\n        tmpl       = template.New(\"\")\n    )\n    \n    func getenvWithDefault(name, defaultValue string) string {\n            val := os.Getenv(name)\n            if val == \"\" {\n                    val = defaultValue\n            }\n            return val\n    }\n\n    func main() {\n        flag.Parse()\n        var err error\n        _, err = tmpl.ParseGlob(filepath.Join(\".\", \"templates\", \"*.html\"))\n        if err != nil {\n            log.Fatalf(\"Unable to parse templates: %v\\n\", err)\n        }\n        http.HandleFunc(\"/\", handler)\n        log.Printf(\"listening on %s\\n\", *listenAddr)\n        http.ListenAndServe(*listenAddr, nil)\n    }\n    func handler(w http.ResponseWriter, r *http.Request) {\n        tmpl.ExecuteTemplate(w, \"index.html\", nil)\n    }","spans":[]},{"type":"paragraph","text":"Run go run main.go again and you should see the template we've configured.","spans":[]},{"type":"image","url":"https://images.prismic.io/www-static/4d14d033-b87d-4e16-ad5c-84cad8b14f7a_Kamal-2.png?auto=compress,format","alt":null,"copyright":null,"dimensions":{"width":1600,"height":994}},{"type":"heading1","text":"Contacts in a Database","spans":[]},{"type":"paragraph","text":"Something is missing in the page—the actual contacts! Let's add them in.","spans":[]},{"type":"paragraph","text":"We will use DigitalOcean Databases to quickly get a PostgreSQL cluster up. If you haven’t yet, create a new one—it only takes a few minutes: if you prefer a text post, see the product documentation for Databases. If you prefer a video, click here.","spans":[{"start":172,"end":211,"type":"hyperlink","data":{"link_type":"Web","url":"https://www.digitalocean.com/docs/databases/how-to/clusters/create/"}},{"start":236,"end":246,"type":"hyperlink","data":{"link_type":"Web","url":"https://www.youtube.com/watch?v=jY5FhyiEdig"}}]},{"type":"image","url":"https://images.prismic.io/www-static/a010b376-74b2-4837-892f-db1d7d8da99e_Kamal-3.png?auto=compress,format","alt":null,"copyright":null,"dimensions":{"width":1600,"height":427}},{"type":"paragraph","text":"Once you've created the cluster, copy its Connection String from the control panel. In the Connection Details section in the Overview page, choose \"Connection string\" from the list and copy it:","spans":[]},{"type":"image","url":"https://images.prismic.io/www-static/2c01727f-4ff8-4f5c-ada2-80ff7f171168_Kamal-4.png?auto=compress,format","alt":null,"copyright":null,"dimensions":{"width":990,"height":540}},{"type":"paragraph","text":"The connection string contains all the details necessary to connect to your database (including your password) so be sure to keep it safe.","spans":[]},{"type":"heading2","text":"Initializing the Database","spans":[]},{"type":"paragraph","text":"Our Go app will only handle displaying the contacts, so I have prepared an SQL export containing 10 randomly generated contacts that you can import into your database. You can find it here.","spans":[{"start":184,"end":188,"type":"hyperlink","data":{"link_type":"Web","url":"https://raw.githubusercontent.com/digitalocean/databases/master/examples/contacts.sql"}}]},{"type":"paragraph","text":"On macOS, I like to use TablePlus to work with my databases, but you can use any client you prefer or import it using the ```[php]{`psql`}``` CLI command like so:","spans":[]},{"type":"preformatted","text":"psql 'your connection string here' < contacts.sql","spans":[]},{"type":"heading2","text":"Fetching the Contacts","spans":[]},{"type":"paragraph","text":"Ok, so now we have a database with some contacts in it 🎉 Let's have our program connect to it and fetch the contacts. We'll build this functionality step by step.","spans":[]},{"type":"paragraph","text":"There are many ways to connect to a PostgreSQL database in Go. In this case, we also need a convenient way to access JSONB fields since our contacts database uses them. I personally found the combination of `github.com/jmoiron/sqlx` and `github.com/jackc/pgx` to work best.","spans":[{"start":207,"end":232,"type":"hyperlink","data":{"link_type":"Web","url":"http://github.com/jmoiron/sqlx"}},{"start":237,"end":259,"type":"hyperlink","data":{"link_type":"Web","url":"http://github.com/jackc/pgx"}}]},{"type":"paragraph","text":"Start by importing the packages:","spans":[]},{"type":"preformatted","text":"go get -u -v github.com/jackc/pgx github.com/jmoiron/sqlx","spans":[]},{"type":"paragraph","text":"And adding them at the top of main.go:","spans":[]},{"type":"preformatted","text":"    import (\n        ...\n\n        _ \"github.com/jackc/pgx/stdlib\"\n        \"github.com/jmoiron/sqlx\"\n        \"github.com/jmoiron/sqlx/types\"\n    )","spans":[]},{"type":"paragraph","text":"Now, there are a few things that we need to do. We need to define the Contact type based on the database's table structure and connect to our PostgreSQL database. When serving the contacts page, we will query the database for the contacts and pass them to the template for rendering.","spans":[]},{"type":"heading3","text":"Contact Type","spans":[]},{"type":"paragraph","text":"Add these types to main.go. They match the structure of the contacts database export and prepare support for the JSONB field favorites:  ","spans":[{"start":56,"end":84,"type":"hyperlink","data":{"link_type":"Web","url":"https://raw.githubusercontent.com/digitalocean/databases/master/examples/contacts.sql"}}]},{"type":"preformatted","text":"    // ContactFavorites is a field that contains a contact's favorites\n    type ContactFavorites struct {  \n        Colors []string `json:\"colors\"`\n    }\n    // Contact represents a Contact model in the database \n    type Contact struct {  \n        ID                   int\n        Name, Address, Phone string\n        FavoritesJSON types.JSONText    `db:\"favorites\"`\n        Favorites     *ContactFavorites `db:\"-\"`\n        CreatedAt string `db:\"created_at\"`\n        UpdatedAt string `db:\"updated_at\"`\n    }","spans":[]},{"type":"heading3","text":"Database Connection","spans":[]},{"type":"paragraph","text":"Note that we haven't connected to the database yet 👀 Let's do that now. We'll pass in the PostgreSQL connection string as a CLI flag and add a global database variable. So again at the top of main.go:","spans":[]},{"type":"preformatted","text":"    var (\n        connectionString = flag.String(\"conn\", getenvWithDefault(\"DATABASE_URL\", \"\"), \"PostgreSQL connection string\")\n        listenAddr       = flag.String(\"addr\", \":8080\", \"HTTP address to listen on\")\n        db               *sqlx.DB\n        tmpl             = template.New(\"\")\n    )","spans":[]},{"type":"paragraph","text":"Note that we use the function getenvWithDefault like with the listen address to allow the connection string to be passed using an environment variable (DATABASE_URL) in addition to the CLI flag (-conn).","spans":[]},{"type":"paragraph","text":"After the templating logic in main()(right above http.HandleFunc()), add the following:","spans":[]},{"type":"preformatted","text":"    if *connectionString == \"\" {\n        log.Fatalln(\"Please pass the connection string using the -conn option\")\n    }\n    \n    db, err = sqlx.Connect(\"pgx\", *connectionString)\n    if err != nil {\n        log.Fatalf(\"Unable to establish connection: %v\\n\", err)\n    }","spans":[]},{"type":"paragraph","text":"We're now connected to our PostgreSQL database!","spans":[]},{"type":"heading3","text":"Querying the Database for Contacts","spans":[]},{"type":"paragraph","text":"Add a new function to the bottom of the file to fetch all contacts from the database. For clearer errors, we'll make use of another package: github.com/pkg/errors. Download it and import it at the top of main.go as usual.","spans":[]},{"type":"preformatted","text":"    go get -u -v github.com/pkg/errors\n    \n    import (\n        ...\n        \"github.com/pkg/errors\"\n        ...\n    )\n    \n    …\n    \n    func fetchContacts() ([]*Contact, error) {\n        contacts := []*Contact{}\n        err := db.Select(&contacts, \"select * from contacts\")\n        if err != nil {\n            return nil, errors.Wrap(err, \"Unable to fetch contacts\")\n        }\n    \n        return contacts, nil\n    }","spans":[]},{"type":"paragraph","text":"One thing that's missing right now is the favorites column. If you look at the Contact type, we've defined this field: FavoritesJSON types.JSONText db:\"favorites\". This maps the favorites column in the database to the FavoritesJSON field in the Contact struct, making it available as a JSON object serialized as text.","spans":[]},{"type":"paragraph","text":"This means that we need to manually parse and un-marshal the JSON objects into actual Go structs. We will use Go’s encoding/json package so make sure to import it at the top of main.go. Adding onto fetchContacts():","spans":[]},{"type":"preformatted","text":"    import (\n        ...\n        \"encoding/json\"\n        ...\n    )\n    ...\n    func fetchContacts() ([]*Contact, error) {\n        ...\n    \n        for _, contact := range contacts {\n            err := json.Unmarshal(contact.FavoritesJSON, &contact.Favorites)\n    \n            if err != nil {\n                return nil, errors.Wrap(err, \"Unable to parse JSON favorites\")\n            }\n        }\n    \n        return contacts, nil\n    }","spans":[]},{"type":"paragraph","text":"The resulting structs will be stored in the Favorites field in the Contact struct.","spans":[]},{"type":"heading2","text":"Rendering the Contacts","spans":[]},{"type":"paragraph","text":"Cool, we have data. Let's use it! Inside the handler() function, we'll use fetchContacts() to get the contacts and then pass them to the template:","spans":[]},{"type":"preformatted","text":"    func handler(w http.ResponseWriter, r *http.Request) {\n        contacts, err := fetchContacts()\n        if err != nil {\n            w.WriteHeader(http.StatusInternalServerError)\n            w.Write([]byte(err.Error()))\n            return\n        }\n    \n        tmpl.ExecuteTemplate(w, \"index.html\", struct{ Contacts []*Contact }{contacts})\n    }","spans":[]},{"type":"paragraph","text":"This will attempt to fetch the contacts, display an error on failure, and pass them to the template. Note that if an error occurs, the full error will be sent as the response. In a production environment you will want to log the error and send a generic error message instead.","spans":[]},{"type":"paragraph","text":"Now we need to modify the template to do something with the contacts we are passing to it. To display favorite colors as a comma-separated list, we'll use the strings.Join function. Before we are able to use it inside the template, we need to define it as a template function, inside main() above the tmpl.ParseGlob line. Don’t forget to import the strings package at the top:","spans":[]},{"type":"preformatted","text":"    import (\n        ...\n        \"strings\"\n        ...\n    )\n    \n    …\n    \n    tmpl.Funcs(template.FuncMap{\"StringsJoin\": strings.Join})\n    _, err = tmpl.ParseGlob(filepath.Join(\".\", \"templates\", \"*.html\"))\n    \n    ...","spans":[]},{"type":"paragraph","text":"Then, under the <h1> line in the HTML template, add the following:","spans":[]},{"type":"preformatted","text":"    {{range .Contacts}}\n    <div class=\"pa2 mb3 striped--near-white\">\n        <header class=\"b mb2\">{{.Name}}</header>\n        <div class=\"pl2\">\n            <p class=\"mb2\">{{.Phone }}</p>\n            <p class=\"pre mb3\">{{.Address}}</p>\n            <p class=\"mb2\"><span class=\"fw5\">Favorite colors:</span> {{StringsJoin .Favorites.Colors \", \"}}</p>\n        </div>\n    </div>\n    {{end}}","spans":[]},{"type":"paragraph","text":"That's all! The final main.go file should look like so:","spans":[]},{"type":"preformatted","text":"    package main\n    \n    import (\n        \"encoding/json\"\n        \"flag\"\n        \"log\"\n        \"html/template\"\n        \"net/http\"\n        \"path/filepath\"\n        \"strings\"\n    \n        _ \"github.com/jackc/pgx/stdlib\"\n        \"github.com/jmoiron/sqlx\"\n        \"github.com/jmoiron/sqlx/types\"\n        \"github.com/pkg/errors\"\n    )\n    \n    // ContactFavorites is a field that contains a contact's favorites\n    type ContactFavorites struct {\n        Colors []string `json:\"colors\"`\n    }\n    \n    // Contact represents a Contact model in the database    \n    type Contact struct {\n        ID                   int\n        Name, Address, Phone string\n    \n        FavoritesJSON types.JSONText    `db:\"favorites\"`\n        Favorites     *ContactFavorites `db:\"-\"`\n    \n        CreatedAt string `db:\"created_at\"`\n        UpdatedAt string `db:\"updated_at\"`\n    }\n    \n    var (\n        connectionString = flag.String(\"conn\", getenvWithDefault(\"DATABASE_URL\", \"\"), \"PostgreSQL connection string\")\n        listenAddr       = flag.String(\"addr\", getenvWithDefault(\"LISTENADDR\", \":8080\"), \"HTTP address to listen on\")\n        db               *sqlx.DB\n        tmpl             = template.New(\"\")\n    )\n    \n    func getenvWithDefault(name, defaultValue string) string {\n            val := os.Getenv(name)\n            if val == \"\" {\n                    val = defaultValue\n            }\n    \n            return val\n    }\n    \n    func main() {\n        flag.Parse()\n        var err error\n    \n        // templating\n    \n        tmpl.Funcs(template.FuncMap{\"StringsJoin\": strings.Join})\n        _, err = tmpl.ParseGlob(filepath.Join(\".\", \"templates\", \"*.html\"))\n        if err != nil {\n            log.Fatalf(\"Unable to parse templates: %v\\n\", err)\n        }\n    \n        // postgres connection\n    \n        if *connectionString == \"\" {\n            log.Fatalln(\"Please pass the connection string using the -conn option\")\n        }\n    \n        db, err = sqlx.Connect(\"pgx\", *connectionString)\n        if err != nil {\n            log.Fatalf(\"Unable to establish connection: %v\\n\", err)\n        }\n    \n        // http server\n    \n        http.HandleFunc(\"/\", handler)\n    \n        log.Printf(\"listening on %s\\n\", *listenAddr)\n        http.ListenAndServe(*listenAddr, nil)\n    }\n    \n    func fetchContacts() ([]*Contact, error) {\n        contacts := []*Contact{}\n        err := db.Select(&contacts, \"select * from contacts\")\n        if err != nil {\n            return nil, errors.Wrap(err, \"Unable to fetch contacts\")\n        }\n    \n        for _, contact := range contacts {\n            err := json.Unmarshal(contact.FavoritesJSON, &contact.Favorites)\n    \n            if err != nil {\n                return nil, errors.Wrap(err, \"Unable to parse JSON favorites\")\n            }\n        }\n    \n        return contacts, nil\n    }\n    \n    func handler(w http.ResponseWriter, r *http.Request) {\n        contacts, err := fetchContacts()\n        if err != nil {\n            w.WriteHeader(http.StatusInternalServerError)\n            w.Write([]byte(err.Error()))\n            return\n        }\n    \n        tmpl.ExecuteTemplate(w, \"index.html\", struct{ Contacts []*Contact }{contacts})\n    }","spans":[]},{"type":"paragraph","text":"Run the program again, passing in your database's connection string like so and you should see the contacts list:","spans":[]},{"type":"preformatted","text":"    go run main.go -conn \"connection string here\"\n    # alternatively:\n    DATABASE_URL=\"connection string here\" go run main.go","spans":[]},{"type":"image","url":"https://images.prismic.io/www-static/3c4e728b-5295-49b9-af19-4ea7e7b4a730_Kamal-5.png?auto=compress,format","alt":null,"copyright":null,"dimensions":{"width":1600,"height":1396}},{"type":"heading1","text":"Conclusion","spans":[]},{"type":"paragraph","text":"After following this post, you will have learned how to build a simple contacts list step-by-step, starting with an empty page served by an HTTP web-server and ending with one that renders a list of contacts fetched from a PostgreSQL database. Along the way, you will have become familiar with using html/template to render a web page with dynamic data, connecting to a PostgreSQL database, and interacting with JSONB objects stored in the database.","spans":[]},{"type":"heading2","text":"Next Steps","spans":[]},{"type":"paragraph","text":"Here are some things you can do after following this post for further practice:","spans":[]},{"type":"list-item","text":"Print favorite colors as a bullet point list with each color being a separate item. Use html/template's built-in range function to loop over the favorite colors slice.","spans":[]},{"type":"list-item","text":"Add a favorite shape (square, circle, etc.) to one or more contacts and edit the template to display it. The Contact struct should stay unmodified.","spans":[]},{"type":"list-item","text":"List the contacts in the order that they were last updated, most recent first.","spans":[]},{"type":"paragraph","text":"[Hungry for more tutorials? Try Kamal's guides to \"Creating a Simple Contacts List with Laravel and PostgreSQL\" and \"Deploying a Fully-automated Git-based Static Website in Under 5 Minutes\"]","spans":[{"start":50,"end":111,"type":"hyperlink","data":{"link_type":"Web","url":"https://blog.digitalocean.com/create-simple-contacts-laravel-postgresql/"}},{"start":116,"end":189,"type":"hyperlink","data":{"link_type":"Web","url":"https://blog.digitalocean.com/deploying-a-fully-automated-git-based-static-website-in-under-5-minutes/"}}]},{"type":"paragraph","text":"Kamal Nasser is a Developer Advocate at DigitalOcean. He is also a Computer Science student with a passion for software engineering and avocados. You can find him on Twitter @kamaln7.","spans":[{"start":0,"end":183,"type":"em"},{"start":174,"end":182,"type":"hyperlink","data":{"link_type":"Web","url":"https://twitter.com/kamaln7"}}]}],"blog_post_date":"2019-03-18","tags":[{"tag1":{"tag":"Developer Relations","_linkType":"Link.document","_meta":{"uid":"developer-relations"}}},{"tag1":{"tag":"Community","_linkType":"Link.document","_meta":{"uid":"community"}}}],"_meta":{"uid":"create-a-simple-contacts-list-with-go"}}},{"node":{"author":{"_linkType":"Link.document","author_name":"Shahidh K Muhammed, Hasura","author_image":null,"_meta":{"uid":"shahidh_k_muhammed_hasura"}},"blog_header_image":{"dimensions":{"width":784,"height":418},"alt":null,"copyright":null,"url":"https://images.prismic.io/www-static/481e269f-ebcc-43f8-b5a6-26fbef338309_CodeReview_blog.png?auto=compress,format"},"blog_headline":[{"type":"heading1","text":"Learning GraphQL By Doing","spans":[]}],"blog_post_content":[{"type":"paragraph","text":"In this tutorial, we’ll cover the basic concepts required for app developers to understand GraphQL, with the intention of learning what a GraphQL API looks like—and how it compares to REST-API equivalents—by actually trying it out.","spans":[{"start":91,"end":98,"type":"hyperlink","data":{"link_type":"Web","url":"https://graphql.org/"}}]},{"type":"paragraph","text":"This article is broken into three parts:","spans":[]},{"type":"o-list-item","text":"What is GraphQL and how it works from the client  ","spans":[]},{"type":"o-list-item","text":"Setting up a GraphQL endpoint on Postgres with Hasura and DigitalOcean  ","spans":[]},{"type":"o-list-item","text":"Exploring GraphQL queries (reads), mutations (writes), and subscriptions (real-time)","spans":[]},{"type":"heading3","text":"What is GraphQL?","spans":[]},{"type":"paragraph","text":"GraphQL is a query language and a runtime for executing the queries. Born at Facebook in 2012 and open sourced in 2015, GraphQL represents data not in terms of resource URLs, secondary keys, or join tables; but in terms of a graph of objects and the models that are ultimately used in apps, like NSObjects or JSON. GraphQL is not bound to any data exchange or transport specifications. Typically in an HTTP context, the GraphQL “query string” is POST-ed to the server and the server responds with JSON data.","spans":[]},{"type":"paragraph","text":"There are three key benefits that GraphQL users observe over REST-ful APIs:","spans":[]},{"type":"o-list-item","text":"The speed of building and iterating on the frontend app dramatically increases  ","spans":[]},{"type":"o-list-item","text":"The amount of data sent from the server to client apps reduces, making apps faster and more responsive  ","spans":[]},{"type":"o-list-item","text":"The communication between frontend and backend teams gets streamlined","spans":[]},{"type":"paragraph","text":"Here’s a GIF showing how a GraphQL client can make precise queries to a GraphQL API server to fetch exactly the data it needs and in the “shape” that it wants.","spans":[]},{"type":"image","url":"https://images.prismic.io/www-static/8a737101-cf78-49d3-9df6-412f239833a1_graphql-api.gif?auto=compress,format","alt":null,"copyright":null,"dimensions":{"width":988,"height":784}},{"type":"paragraph","text":"GraphQL adopters see these benefits because:","spans":[]},{"type":"list-item","text":"A GraphQL API allows the frontend developer to easily fetch required data across multiple resources in just a single API call instead of making multiple REST API calls.","spans":[]},{"type":"list-item","text":"A GraphQL API supports subscriptions as a standard part of the spec that makes it easy to consume real-time information on the frontend, without having to bother with setting up complex networking clients manually.","spans":[]},{"type":"list-item","text":"A GraphQL server supports introspection and has a type system that makes it possible to have great tooling for API consumers. Community tools use these introspection APIs to give you everything from auto-complete to API exploration to codegen so that you don’t have to create request/response classes for your APIs!","spans":[]},{"type":"paragraph","text":"The best way of understanding new technology like GraphQL is by trying it out. In this tutorial, we’re going to use Hasura on DigitalOcean to explore GraphQL.","spans":[{"start":116,"end":122,"type":"hyperlink","data":{"link_type":"Web","url":"https://hasura.io/"}}]},{"type":"heading3","text":"Set up a GraphQL Server","spans":[]},{"type":"paragraph","text":"Let’s first set up a GraphQL server and get a hang of it before we deep dive into more GraphQL. We’re going to use the Hasura GraphQL Engine, which is available as a 1-Click application. It is packed with a Postgres database and Caddy web server for easy and automatic HTTPS using Let’s Encrypt.","spans":[{"start":142,"end":185,"type":"hyperlink","data":{"link_type":"Web","url":"https://marketplace.digitalocean.com/apps/hasura-graphql"}},{"start":229,"end":234,"type":"hyperlink","data":{"link_type":"Web","url":"https://caddyserver.com/"}}]},{"type":"paragraph","text":"[Related: Want more Caddy? Check out the tutorial \"Deploying a Fully-automated Git-based Static Website in Under 5 Minutes\"]","spans":[{"start":0,"end":124,"type":"strong"},{"start":51,"end":122,"type":"hyperlink","data":{"link_type":"Web","url":"https://blog.digitalocean.com/deploying-a-fully-automated-git-based-static-website-in-under-5-minutes/"}}]},{"type":"paragraph","text":"Create your Hasura GraphQL Droplet, which installs all the required software and packages itself using Docker. Once the Droplet is ready, head to the Droplet IP on a browser. The Hasura console will open up, and this is where we will interact with the Hasura GraphQL Server to create the schema, test out APIs, manage data etc.","spans":[{"start":12,"end":34,"type":"hyperlink","data":{"link_type":"Web","url":"https://marketplace.digitalocean.com/apps/hasura?action=deploy&amp;refcode=c4d9092d2c48&amp;utm_source=hasura&amp;utm_campaign=do-blog"}}]},{"type":"image","url":"https://images.prismic.io/www-static/73fc240c-6622-470b-be20-b83c28937d88_graphiql_tab.png?auto=compress,format","alt":"A screenshot of the Hasura console showing the default GraphQL tab where users can try out GraphQL queries","copyright":null,"dimensions":{"width":1266,"height":912}},{"type":"paragraph","text":"A screenshot of the Hasura console showing the default GraphiQL tab where users can try out GraphQL queries.","spans":[{"start":0,"end":108,"type":"em"}]},{"type":"paragraph","text":"This tab where we try out the GraphQL queries is called GraphiQL. It serves as a playground/documentation for the GraphQL server with features like validation, autocomplete, etc.. Throughout this guide, we’ll use GraphiQL to try and test out our queries.","spans":[{"start":56,"end":64,"type":"strong"}]},{"type":"paragraph","text":"There is a public endpoint setup at https://learn-graphql.demo.hasura.app so that users can try out the steps in this tutorial without actually creating a Droplet.","spans":[{"start":36,"end":73,"type":"hyperlink","data":{"link_type":"Web","url":"https://learn-graphql.demo.hasura.app"}}]},{"type":"heading3","text":"Create a table","spans":[]},{"type":"paragraph","text":"Hasura creates the GraphQL schema by looking at Postgres schema. If there is a table called product, there will be a GraphQL type called product. We’ll come back to this again after we’ve made our first GraphQL query.","spans":[]},{"type":"paragraph","text":"Let's create a table in Postgres and get GraphQL on that, all through the Hasura console.","spans":[]},{"type":"paragraph","text":"Navigate to *Data -> Create* table on the console and create a table called `team` with the following columns:","spans":[]},{"type":"paragraph","text":"Column Name         Type","spans":[]},{"type":"paragraph","text":"id                              Integer","spans":[]},{"type":"paragraph","text":"(auto-increment)    nameText","spans":[]},{"type":"paragraph","text":"Other inputs and checkboxes like default value, nullable and, unique can be ignored. Choose `id` as the Primary Key and click the *Create* button.","spans":[]},{"type":"paragraph","text":"","spans":[]},{"type":"image","url":"https://images.prismic.io/www-static/b7927157-1a2e-433c-9157-aad62c73e40b_create_team_table.png?auto=compress,format","alt":null,"copyright":null,"dimensions":{"width":1268,"height":834}},{"type":"paragraph","text":"Screenshot of console showing the create table screen with details filled in.","spans":[{"start":0,"end":77,"type":"em"}]},{"type":"heading3","text":"Insert sample data","spans":[]},{"type":"paragraph","text":"Once the table is created, you will be taken to the table's dashboard. Let's go to the *Insert Row* tab and add some rows. The ```[php]{`id`}``` column will be disabled as it is going to be auto-generated. You will only be able to enter the name. Once you enter one name, say Avengers, click the Save button. You will see a notification on the top right corner saying that the row has been inserted! ","spans":[{"start":296,"end":300,"type":"em"}]},{"type":"paragraph","text":"The text will remain in the name input box and the button will change to Insert Again from Save. Edit the name input box to add the next team, Justice League, and click the Insert Again button.","spans":[{"start":73,"end":85,"type":"em"},{"start":91,"end":95,"type":"em"},{"start":173,"end":185,"type":"em"}]},{"type":"image","url":"https://images.prismic.io/www-static/98025e01-5ddf-415d-bc54-276eaac7214a_insert_team.png?auto=compress,format","alt":null,"copyright":null,"dimensions":{"width":1268,"height":428}},{"type":"paragraph","text":"Inserting rows into the table.","spans":[{"start":0,"end":30,"type":"em"}]},{"type":"paragraph","text":"Now if we go to Browse Rows tab, we can see that we have inserted two teams with name Avengers and Justice League, for which the ```[php]{`id`}``` has been auto-generated. Avengers got ```[php]{`id`}``` 1 and Justice League got ```[php]{`id`}``` 2.","spans":[{"start":16,"end":27,"type":"em"}]},{"type":"image","url":"https://images.prismic.io/www-static/6b1c8cfb-ee7e-47c4-95e9-76aa18d8b894_browse_rows_team.png?auto=compress,format","alt":null,"copyright":null,"dimensions":{"width":1268,"height":584}},{"type":"paragraph","text":"Team table showing two rows.","spans":[{"start":0,"end":28,"type":"em"}]},{"type":"paragraph","text":"This is standard Postgres stuff. Let's jump into GraphQL now.","spans":[]},{"type":"heading3","text":"Try out GraphQL","spans":[]},{"type":"paragraph","text":"Switch to the GraphiQL tab by clicking the link on the top of the console. You will see a URL on top, which is the GraphQL API endpoint; a section to add HTTP headers; and, then the GraphiQL screen split into left and right sides. We enter the GraphQL query on the left side editor and then click the *Play* button on top and the API response appears on the right side.","spans":[]},{"type":"paragraph","text":"Copy the following GraphQL query and paste into the left side of GraphiQL. Now, click on the button with *Play* icon on it. This will send the query to the server and show the response on the right side.","spans":[]},{"type":"preformatted","text":"```[php]{`","spans":[]},{"type":"paragraph","text":"    query {  ","spans":[]},{"type":"paragraph","text":"     team {","spans":[]},{"type":"paragraph","text":"       id","spans":[]},{"type":"paragraph","text":"       name","spans":[]},{"type":"paragraph","text":"     }","spans":[]},{"type":"paragraph","text":"    }","spans":[]},{"type":"preformatted","text":"`}```","spans":[]},{"type":"paragraph","text":"The response will appear on the right.","spans":[]},{"type":"image","url":"https://images.prismic.io/www-static/dc8f5787-e7ee-4fec-83b7-fd2ca5445ceb_team_query.png?auto=compress,format","alt":null,"copyright":null,"dimensions":{"width":1256,"height":812}},{"type":"paragraph","text":"Executing the query to get all teams.","spans":[{"start":0,"end":37,"type":"em"}]},{"type":"paragraph","text":"Woohoo! You have made your first GraphQL query on Hasura. The query above is asking the server to give information on all team objects along with the ```[php]{`id`}``` and ```[php]{`name`}``` columns.","spans":[]},{"type":"heading3","text":"GraphQL Query","spans":[]},{"type":"paragraph","text":"A GraphQL query is a string that is sent to a server to be interpreted and fulfilled, which then returns JSON back to the client. It is designed for developers of web/mobile apps (HTTP clients) to be able to make API calls to fetch the data they need from their backend APIs in a more convenient way.","spans":[]},{"type":"paragraph","text":"Let’s take a closer look at what that means. Let's say you have an API to fetch all teams. This is a typical way to do it with REST: (Request is shown on the left and response on the right).","spans":[]},{"type":"image","url":"https://images.prismic.io/www-static/89529091-c86f-4caf-a946-98cde2d1f67f_code_table_1.png?auto=compress,format","alt":null,"copyright":null,"dimensions":{"width":996,"height":280}},{"type":"paragraph","text":"But, with a GraphQL API, the request would look like this:","spans":[]},{"type":"image","url":"https://images.prismic.io/www-static/2f638848-2ca2-4d24-808e-1e7778d7c703_code_table_2.png?auto=compress,format","alt":null,"copyright":null,"dimensions":{"width":994,"height":349}},{"type":"paragraph","text":"This is what GraphiQL is doing under the hood. It is taking the query string; wrapping it in a JSON object, with a top-level query key; and, POST-ing it to the server. All GraphQL client libraries takes care of this and, as a user, we will only have to deal with the query string.","spans":[]},{"type":"paragraph","text":"Instead of GET-ing a resource at a URL qualified by the resource, we POST a GraphQL query to a single endpoint. The query defines the resource we want with any parameter of subresources. It is not JSON, but a string representation of the data we need, wrapped in a JSON body. The server responds with a JSON in the same shape of the data we requested: there is ```[php]{`team`}``` key at top level, then ```[php]{`id`}``` and ```[php]{`name`}``` nested inside.","spans":[]},{"type":"heading3","text":"Types of queries","spans":[]},{"type":"paragraph","text":"There are three different types of GraphQL queries.","spans":[]},{"type":"o-list-item","text":"Query  ","spans":[]},{"type":"o-list-item","text":"Mutation  ","spans":[]},{"type":"o-list-item","text":"Subscription","spans":[]},{"type":"paragraph","text":"Queries are typically used to fetch data, mutations for writing data and subscriptions for fetching real-time data. All three of them are collectively called “queries” again.","spans":[]},{"type":"heading3","text":"Parts of a query","spans":[]},{"type":"paragraph","text":"Each GraphQL query, whether it is a query, mutation, or subscription, fundamentally contains fields and arguments. ","spans":[]},{"type":"heading4","text":"Fields","spans":[]},{"type":"paragraph","text":"A field simply indicate that we are asking the server for that particular info. For example, in the following query that we ran earlier:","spans":[]},{"type":"image","url":"https://images.prismic.io/www-static/23b1c52d-b779-4099-9448-c1e12f09b63c_fields.png?auto=compress,format","alt":null,"copyright":null,"dimensions":{"width":1002,"height":288}},{"type":"paragraph","text":"We ask the server for the field ```[php]{`team`}``` and its subfields ```[php]{`id`}``` and ```[php]{`name`}```, and the server returns data in the same shape as we asked for. ","spans":[]},{"type":"heading4","text":"Arguments","spans":[]},{"type":"paragraph","text":"In something like REST, we could only pass a single set of argument - as query parameters and URL segments. For example, to get a particular profile, a typical REST call would look like the following:","spans":[]},{"type":"image","url":"https://prismic-io.s3.amazonaws.com/www-static/50169033-6c0d-4d24-81d1-c1dea4ff5b41_argument_1.png","alt":null,"copyright":null,"dimensions":{"width":1003,"height":129}},{"type":"paragraph","text":"Or like this:","spans":[]},{"type":"image","url":"https://prismic-io.s3.amazonaws.com/www-static/b1df8108-4511-44aa-9ae4-5d801c763829_argument_2.png","alt":null,"copyright":null,"dimensions":{"width":998,"height":137}},{"type":"paragraph","text":"With GraphQL, every field can take any arbitrary number of arguments as defined by the schema. The REST API above would look like this with GraphQL:","spans":[]},{"type":"image","url":"https://images.prismic.io/www-static/efcd3838-d8d0-4e45-9246-59260243ef8c_argument_3.png?auto=compress,format","alt":null,"copyright":null,"dimensions":{"width":996,"height":219}},{"type":"paragraph","text":"Try this out in GraphiQL.","spans":[]},{"type":"image","url":"https://images.prismic.io/www-static/9e140e9d-5a6e-4ae5-b29d-c13f043202ac_team_where_query.png?auto=compress,format","alt":null,"copyright":null,"dimensions":{"width":1256,"height":747}},{"type":"paragraph","text":"A query using the where argument.","spans":[{"start":0,"end":33,"type":"em"}]},{"type":"paragraph","text":"Similarly, there are many other arguments available for queries, like ```[php]{`order_by`}```, ```[php]{`limit`}```, ```[php]{`offset`}``` etc. You can try them out in GraphiQL: use Ctrl/Cmd+Space to open up the auto-complete suggestions.","spans":[]},{"type":"heading3","text":"Query variables","spans":[]},{"type":"paragraph","text":"But how do we parametrize this? Since the query is a string, a naive way would be to implement string interpolation. But this can introduce bugs and is not efficient.","spans":[]},{"type":"paragraph","text":"Any GraphQL query can be parametrized by defining variables. Variables can be used in the query string and the values of those variables can be sent in a separate object. This makes it easy to re-use queries by using different set of variables instead of something like string interpolation. Variables need to be defined before they can be used and while defining the type also need to be provided. ","spans":[]},{"type":"paragraph","text":"Here is an example:","spans":[]},{"type":"image","url":"https://images.prismic.io/www-static/c8d5f781-31f5-4cae-b01d-572913f3278a_query.png?auto=compress,format","alt":null,"copyright":null,"dimensions":{"width":995,"height":307}},{"type":"paragraph","text":"The first column contains the query, the column below that contains the variables object, and the right column contains the response, as represented in GraphiQL. Try this query out and see the response:","spans":[]},{"type":"image","url":"https://images.prismic.io/www-static/2d6f52f5-de77-4d8b-814e-4127ca5926a0_team_variable_query.png?auto=compress,format","alt":null,"copyright":null,"dimensions":{"width":1256,"height":809}},{"type":"paragraph","text":"A query along with variables.","spans":[{"start":0,"end":29,"type":"em"}]},{"type":"paragraph","text":"Here we are defining a variable called ```[php]{`id`}```, as denoted by the prefix ```[php]{`$`}``` and it is of type ```[php]{`Int`}```. The ```[php]{`!`}``` indicates that this variable is mandatory for this query to be executed. The client typically send the variables in a JSON object with key ```[php]{`variables`}``` in the JSON POST body along with the ```[php]{`query`}``` string.","spans":[]},{"type":"paragraph","text":"The POST body would look something like this:","spans":[]},{"type":"image","url":"https://images.prismic.io/www-static/9399d71a-5308-4790-b6dd-04711cc60253_query_2.png?auto=compress,format","alt":null,"copyright":null,"dimensions":{"width":979,"height":172}},{"type":"paragraph","text":"Now, the query string need only be defined once in your application and for various ```[php]{`id`}```s, the variables can be changed as required.","spans":[]},{"type":"heading3","text":"Query","spans":[]},{"type":"paragraph","text":"A query is a GraphQL query typically used to fetch data. All the examples we saw earlier are \"queries.\" For example, when we get the team with ```[php]{`id`}``` 2:","spans":[]},{"type":"image","url":"https://images.prismic.io/www-static/c5f35492-6e4b-4aa1-a463-078825b971ba_query_3.png?auto=compress,format","alt":null,"copyright":null,"dimensions":{"width":993,"height":217}},{"type":"heading3","text":"Multiple root nodes","spans":[]},{"type":"paragraph","text":"Let's take a look at how we can query multiple tables (or nodes) in the same query. For that, let us create a new table first.","spans":[]},{"type":"paragraph","text":"Go back to the Data tab and create a new table called ```[php]{`superhero`}``` with the following columns:","spans":[{"start":15,"end":19,"type":"em"}]},{"type":"paragraph","text":"Column Name      Type","spans":[]},{"type":"paragraph","text":"id                            Integer","spans":[]},{"type":"paragraph","text":"(auto-increment)   nameText","spans":[]},{"type":"paragraph","text":"team_id                   Integer","spans":[]},{"type":"paragraph","text":"Other inputs and checkboxes like default value, nullable, and unique can be ignored. Choose ```[php]{`id`}``` as the Primary Key and click the Create button.","spans":[{"start":143,"end":149,"type":"em"}]},{"type":"image","url":"https://images.prismic.io/www-static/57158fac-75d0-43e1-ae2a-85f18b57b2b4_superhero_table.png?auto=compress,format","alt":null,"copyright":null,"dimensions":{"width":1268,"height":875}},{"type":"paragraph","text":"Creating the superhero table.","spans":[{"start":0,"end":29,"type":"em"}]},{"type":"paragraph","text":"Once the table is created, you'll be taken to the Modify tab. Let's switch to Insert Rows tab and insert some rows like we did last time.","spans":[{"start":50,"end":56,"type":"em"},{"start":78,"end":89,"type":"em"}]},{"type":"paragraph","text":"The ```[php]{`id`}``` column will be disabled like last time, since it will be auto-generated by Postgres. Let's enter the ```[php]{`name`}``` and ```[php]{`team_id`}``` for each super hero. If we scroll back, ```[php]{`team_id`}``` is 1 for Avengers and 2 for Justice League.","spans":[]},{"type":"paragraph","text":"name                     team_id","spans":[]},{"type":"paragraph","text":"Captain America  1","spans":[]},{"type":"paragraph","text":"Black Widow        1","spans":[]},{"type":"paragraph","text":"Batman                 2","spans":[]},{"type":"paragraph","text":"Wonder Woman   2","spans":[]},{"type":"paragraph","text":"Once we add the data, using GraphQL, we can fetch data from multiple nodes (tables) in the same query. Copy the following query into GraphiQL and hit the Play button.","spans":[]},{"type":"image","url":"https://images.prismic.io/www-static/f012daeb-3f0d-4ad9-8019-7b3a97aa264e_query_4.png?auto=compress,format","alt":null,"copyright":null,"dimensions":{"width":995,"height":679}},{"type":"image","url":"https://images.prismic.io/www-static/31324400-3d0f-4426-a45c-23105e3d9f27_multi-nodes.png?auto=compress,format","alt":null,"copyright":null,"dimensions":{"width":1256,"height":1098}},{"type":"paragraph","text":"Using multiple nodes in the same GraphQL query.","spans":[{"start":0,"end":47,"type":"em"}]},{"type":"paragraph","text":"Similarly how many ever such top-level nodes can be included in a single query. This applies to mutations too, but subscription spec limits subscriptions to contain only one top-level node.","spans":[]},{"type":"heading3","text":"Querying related data","spans":[]},{"type":"paragraph","text":"For data that are related and are in multiple tables, we can define relationships over the columns and query them in a nested format. For e.g. let us see how we can query for the team and get all the superheroes in that team.","spans":[]},{"type":"paragraph","text":"In order to do this, we need to create a relationship between ```[php]{`team`}``` and ```[php]{`superhero`}``` tables. We already know that ```[php]{`team_id`}``` is our link. In relational database modelling, a foreign key constraint is added on this column to indicate the values in this columns should also be present as the ```[php]{`id`}``` column in ```[php]{`team`}``` table. Let's first add that constraint. Note that this is totally optional and will only act as a constraint, as a foreign key is not required to create a relationship. ","spans":[]},{"type":"paragraph","text":"Head to the Data tab, click on the ```[php]{`superhero`}``` table on the left and then go to the Modify tab. On this tab, click on the Edit button next to ```[php]{`team_id`}``` column. This will open up certain properties of that column. The last one among them will be a Foreign Key. Tick the checkbox there and then choose ```[php]{`team`}``` as the reference table and ```[php]{`id`}``` as the reference column. Then click the Save button.","spans":[{"start":12,"end":16,"type":"em"},{"start":97,"end":103,"type":"em"},{"start":135,"end":139,"type":"em"},{"start":431,"end":435,"type":"em"}]},{"type":"image","url":"https://images.prismic.io/www-static/8f54f674-f23f-431a-a58d-5836e3f77ed8_add-fk-team-id.png?auto=compress,format","alt":null,"copyright":null,"dimensions":{"width":1268,"height":1012}},{"type":"paragraph","text":"Creating a Foreign Key constraint on team_id column","spans":[{"start":0,"end":51,"type":"em"}]},{"type":"paragraph","text":"Once the save has happened, a notification appears on the top right saying the constraint is created.","spans":[]},{"type":"paragraph","text":"Now, head to the ```[php]{`team`}``` table by clicking on the left sidebar. Go to the Relationships tab and you'll see a new array relationship suggestion.","spans":[{"start":86,"end":99,"type":"em"}]},{"type":"image","url":"https://images.prismic.io/www-static/3babf3f7-7f01-4bf0-acc4-a2ad40d003a8_team-array-rel-suggestion.png?auto=compress,format","alt":null,"copyright":null,"dimensions":{"width":1268,"height":605}},{"type":"paragraph","text":"Array relationship suggestion on the team table","spans":[{"start":0,"end":47,"type":"em"}]},{"type":"paragraph","text":"Click on the Add button here. A name will be auto-filled for the relationship, let's change it to just ```[php]{`superheroes`}``` and click the Save button.","spans":[{"start":13,"end":16,"type":"em"},{"start":144,"end":148,"type":"em"}]},{"type":"image","url":"https://images.prismic.io/www-static/146153ac-de50-419b-b905-482fdb8206fb_team-array-rel-save.png?auto=compress,format","alt":null,"copyright":null,"dimensions":{"width":1268,"height":617}},{"type":"paragraph","text":"Entering the array relationship name as superheroes","spans":[{"start":0,"end":51,"type":"em"}]},{"type":"paragraph","text":"You'll see a notification on the top right saying the relationship is saved.","spans":[]},{"type":"paragraph","text":"Now, head back to GraphiQL by clicking on the top bar and copy paste the following query and click *Play* button to execute.","spans":[]},{"type":"image","url":"https://images.prismic.io/www-static/706216b8-225f-41de-8dee-ff426f333037_query_5.png?auto=compress,format","alt":null,"copyright":null,"dimensions":{"width":992,"height":475}},{"type":"image","url":"https://images.prismic.io/www-static/e78d2dd4-50e1-4c3c-b257-bc724df2edf5_related-query.png?auto=compress,format","alt":null,"copyright":null,"dimensions":{"width":1256,"height":1012}},{"type":"paragraph","text":"Querying related data","spans":[{"start":0,"end":21,"type":"em"}]},{"type":"paragraph","text":"One query can traverse related objects and their fields, letting clients fetch lots of related data in one request, instead of making several round trips as one would need in a classic REST architecture.","spans":[]},{"type":"heading3","text":"Mutation","spans":[]},{"type":"paragraph","text":"In GraphQL realm, a mutation is a type of query that typically mutates data, like database insert/update/deletes. REST equivalents would be PUT/POST/DELETE requests. Let's say I need to add a new ```[php]{`superhero`}``` to my database. A REST request would look like the following:","spans":[]},{"type":"image","url":"https://images.prismic.io/www-static/d06278d9-5850-48c2-8a2c-606eb58856c4_Post_1.png?auto=compress,format","alt":null,"copyright":null,"dimensions":{"width":994,"height":192}},{"type":"paragraph","text":"Now, let's take a look at the GraphQL equivalent:","spans":[]},{"type":"image","url":"https://images.prismic.io/www-static/3708965e-a4e2-464c-a299-0551b9644513_Post_2.png?auto=compress,format","alt":null,"copyright":null,"dimensions":{"width":994,"height":323}},{"type":"paragraph","text":"We can also have the name as a variable so that we do not have to manipulate strings to insert different Avengers.","spans":[]},{"type":"image","url":"https://images.prismic.io/www-static/efbc3ddf-bd3f-49d6-a35f-43ddcd034364_mutation.png?auto=compress,format","alt":null,"copyright":null,"dimensions":{"width":993,"height":452}},{"type":"paragraph","text":"Like we mentioned before, the first column is the query, the one on the bottom are the variables, and the right columns shows us the response.","spans":[]},{"type":"image","url":"https://images.prismic.io/www-static/4ab28403-6564-4f31-9fa3-f7a8afd3d098_inser-hero-mutation.png?auto=compress,format","alt":null,"copyright":null,"dimensions":{"width":1256,"height":927}},{"type":"paragraph","text":"A mutation inserting a new superhero","spans":[{"start":0,"end":36,"type":"em"}]},{"type":"paragraph","text":"Like queries, we can mix multiple mutations in the same request. You can also insert related data in a single mutation.","spans":[]},{"type":"heading3","text":"Subscription","spans":[]},{"type":"paragraph","text":"Subscriptions provide real-time capabilities in GraphQL. Traditionally there are two options to obtain live data on the client side. One is calling the REST endpoint at regular intervals of time till we get the desired state (also called polling) and the other is to push an event to the client from the server using websockets.","spans":[]},{"type":"paragraph","text":"Polling is a highly inefficient process and implementing websockets are a nightmare. GraphQL subscriptions also works over websockets, but GraphQL community has standardised what the language the websocket server and client should speak. Because of that, all clients and servers are compatible with each other and the integration is seamless.","spans":[]},{"type":"paragraph","text":"Any query can be converted into a subscription with Hasura. For example, the query to get all Avengers can be made into a subscription like this, and if a web component is rendered (say React) using this data, the component re-renders when a new item gets added. This can be experimented on easily by opening two GraphiQL windows: running a subscription on one window, while try making a mutation on the other one.","spans":[]},{"type":"paragraph","text":"The subscription on the left column, when copied onto GraphiQL and executed, is asking the server to send changes on the ```[php]{`team`}``` table, particularly for the one with ```[php]{`id`}``` equals 1, as and when they happen. The mutation on the right, when executed, will insert a new superhero to the ```[php]{`superhero`}``` table. Since the two tables are related through ```[php]{`team_id`}```, Hasura notices that there is a new superhero that has been added to team 1 and pushed that information onto the client. The result of the subscription updates automatically—all in real-time.","spans":[]},{"type":"image","url":"https://images.prismic.io/www-static/a69795d8-deb3-4339-a1a7-3281651ab42d_subscription.png?auto=compress,format","alt":null,"copyright":null,"dimensions":{"width":1000,"height":325}},{"type":"image","url":"https://images.prismic.io/www-static/fc71f1c3-59f2-4de5-9fc2-43cefb109399_Peek-2019-02-19-18-16.gif?auto=compress,format","alt":null,"copyright":null,"dimensions":{"width":1600,"height":812}},{"type":"paragraph","text":"A GIF showing subscriptions","spans":[{"start":0,"end":27,"type":"em"}]},{"type":"paragraph","text":"Under the hood, the GraphQL client and the server communicate over websockets and messages are transferred without any extra involvement from the user. When used in a application, the client library takes care of all the plumbing for the user.","spans":[]},{"type":"heading3","text":"The GraphQL Schema","spans":[]},{"type":"paragraph","text":"Documenting APIs is very important for team collaboration and feature velocity. The backend team works on the API, documents it, and passes it down to the frontend team. But, by the time the frontend team gets to use the APIs, the documentation could be out of date, missing, or plain wrong, since a separate human process is required for maintaining docs.","spans":[]},{"type":"paragraph","text":"GraphQL attempts to address this issues by introducing a type system for the API: the GraphQL schema. It talks about what fields the client can query, whether it is an integer or a string, what are the parameters for this mutation, is this parameter required or not etc.","spans":[]},{"type":"paragraph","text":"In fact, you could make a GraphQL query to get this schema. This is called introspection, where the server publishes its own schema and clients can query it when required. This opens up lot of possibilities, including type safety for your API. Clients can look at this schema and generate code required for making idiomatic type safe API calls and errors could be caught at compile time itself instead of at runtime (for clients written in typed languages). IDEs can exploit this and show live feedback like autocomplete and validation on the queries written by the developer.","spans":[]},{"type":"paragraph","text":"Try out the following query:","spans":[]},{"type":"image","url":"https://images.prismic.io/www-static/28780361-d364-430e-b833-29948f61228d_query_6.png?auto=compress,format","alt":null,"copyright":null,"dimensions":{"width":897,"height":853}},{"type":"paragraph","text":"There is a *Docs* button on the top right corner of GraphiQL tab where you can browse this schema. It is helpful in figuring out what are the queries/mutations/subscriptions supported by the server and what their arguments and return types are.","spans":[]},{"type":"heading3","text":"Conclusion","spans":[]},{"type":"paragraph","text":"In this tutorial, we learned about GraphQL by setting up a GraphQL server over Postgres and by trying out some GraphQL queries, mutations, and subscriptions. We have gone through the types of GraphQL queries and the different parts within a query. We also walked though how a GraphQL schema helps building intelligent tools for the ecosystem, which increases developer productivity.","spans":[]},{"type":"paragraph","text":"As next steps, you'd want to integrate GraphQL with your application, including authentication and authorization. Check out the resources below for further reading.","spans":[]},{"type":"heading3","text":"Resources","spans":[]},{"type":"list-item","text":"Building your schema","spans":[{"start":0,"end":20,"type":"hyperlink","data":{"link_type":"Web","url":"https://docs.hasura.io/1.0/graphql/manual/schema/index.html"}}]},{"type":"list-item","text":"GraphQL Queries","spans":[{"start":0,"end":15,"type":"hyperlink","data":{"link_type":"Web","url":"https://docs.hasura.io/1.0/graphql/manual/queries/index.html"}}]},{"type":"list-item","text":"GraphQL Mutations","spans":[{"start":0,"end":17,"type":"hyperlink","data":{"link_type":"Web","url":"https://docs.hasura.io/1.0/graphql/manual/mutations/index.html"}}]},{"type":"list-item","text":"GraphQL Subscriptions","spans":[{"start":0,"end":21,"type":"hyperlink","data":{"link_type":"Web","url":"https://docs.hasura.io/1.0/graphql/manual/subscriptions/index.html"}}]},{"type":"list-item","text":"Event Triggers","spans":[{"start":0,"end":14,"type":"hyperlink","data":{"link_type":"Web","url":"https://docs.hasura.io/1.0/graphql/manual/event-triggers/index.html"}}]},{"type":"list-item","text":"Remote Schemas","spans":[{"start":0,"end":14,"type":"hyperlink","data":{"link_type":"Web","url":"https://docs.hasura.io/1.0/graphql/manual/remote-schemas/index.html"}}]},{"type":"list-item","text":"Authentication/Access control","spans":[{"start":0,"end":29,"type":"hyperlink","data":{"link_type":"Web","url":"https://docs.hasura.io/1.0/graphql/manual/event-triggers/index.html"}}]},{"type":"list-item","text":"Database Migrations","spans":[{"start":0,"end":19,"type":"hyperlink","data":{"link_type":"Web","url":"https://docs.hasura.io/1.0/graphql/manual/migrations/index.html"}}]},{"type":"list-item","text":"Hasura GraphQL Guides","spans":[{"start":0,"end":21,"type":"hyperlink","data":{"link_type":"Web","url":"https://docs.hasura.io/1.0/graphql/manual/guides/index.html"}}]},{"type":"heading3","text":"Community and Support","spans":[]},{"type":"paragraph","text":"If you have any questions regarding this tutorial or using Hasura, please reach out to the Hasura community:","spans":[]},{"type":"list-item","text":"Github","spans":[{"start":0,"end":6,"type":"hyperlink","data":{"link_type":"Web","url":"https://github.com/hasura/graphql-engine"}}]},{"type":"list-item","text":"Discord (community forum)","spans":[{"start":0,"end":25,"type":"hyperlink","data":{"link_type":"Web","url":"https://discord.gg/hasura"}}]},{"type":"list-item","text":"Twitter","spans":[{"start":0,"end":7,"type":"hyperlink","data":{"link_type":"Web","url":"https://twitter.com/hasurahq"}}]}],"blog_post_date":"2019-03-12","tags":[{"tag1":{"tag":"Marketplace","_linkType":"Link.document","_meta":{"uid":"marketplace"}}}],"_meta":{"uid":"learning-graphql-by-doing"}}},{"node":{"author":{"_linkType":"Link.document","author_name":"Yilun Zhang, NKN","author_image":null,"_meta":{"uid":"yilun_zhang_nkn"}},"blog_header_image":{"dimensions":{"width":800,"height":600},"alt":"api token","copyright":null,"url":"https://images.prismic.io/www-static/1e6268d6-b745-4db1-be2b-ebe796066d37_freebsd_install_dribbble.png?auto=compress,format"},"blog_headline":[{"type":"heading1","text":"Using Packer to Create a 1-Click NKN Image on DigitalOcean","spans":[]}],"blog_post_content":[{"type":"paragraph","text":"NKN is the new kind of P2P network connectivity protocol and ecosystem powered by a novel public blockchain. Our open source node software allows internet users to share network connections and unused bandwidth for rewards. By running an NKN node, you become part of the NKN network community helping to build the decentralized internet so everyone can enjoy secure, low cost, and universally accessible connectivity.","spans":[{"start":0,"end":3,"type":"hyperlink","data":{"link_type":"Web","url":"https://www.nkn.org/#/"}}]},{"type":"paragraph","text":"Recently NKN’s Testnet has grown to over 6,000 community full nodes and many of these nodes are running on DigitalOcean. In fact, one of the most frequently asked questions from our community is how to set up a NKN full node on DigitalOcean? We have a few tutorials on how to do that; however, having to properly and safely set up the Droplet environment creates significant friction for those who have just heard of the project and want to try it for themselves, preferably in just a few minutes.","spans":[{"start":15,"end":22,"type":"hyperlink","data":{"link_type":"Web","url":"https://testnet.nkn.org/"}}]},{"type":"paragraph","text":"Luckily for all, DigitalOcean now provides a Marketplace where we can release a 1-Click App for NKN, a very convenient and user-friendly way to deploy preconfigured droplets in 60 seconds. You can quickly start a NKN 1-Click App from the \"Marketplace\" in your DigitalOcean cloud control panel, or by visiting DigitalOcean Marketplace directly.","spans":[{"start":78,"end":99,"type":"hyperlink","data":{"link_type":"Web","url":"https://marketplace.digitalocean.com/apps/nkn-full-node"}},{"start":322,"end":333,"type":"hyperlink","data":{"link_type":"Web","url":"https://marketplace.digitalocean.com"}}]},{"type":"image","url":"https://images.prismic.io/www-static/29fcdc77-d002-414a-b122-cccfbec7641e_NKN+blog+post+-+DigitalOcean+Marketplace+2019-03-07+10-41-16.png?auto=compress,format","alt":null,"copyright":null,"dimensions":{"width":2762,"height":1786}},{"type":"heading3","text":"Using Packer to Create a 1-Click Image","spans":[]},{"type":"paragraph","text":"To make the NKN 1-Click App in DigitalOcean's new Marketplace, we recognized that creating a preconfigured production-ready image takes effort: not just to create the initial image, but every time that image needs to be updated. A few repetitive but necessary steps need to be done every time a snapshot is created, e.g. starting and stopping Droplets, removing user history and trace, creating a snapshot from Droplets. Doing these steps manually for each image update is not only time consuming, but can open the opportunity for mistakes. ","spans":[]},{"type":"paragraph","text":"Another headache comes when we want to create the same image for multiple cloud providers. Typically each cloud providers has their own steps and requirements when creating an image. For example, DigitalOcean requires an image to have `cloud-init` set up, while Google Cloud requires an image to be licensed. These differences need to be taken into account every time an image is updated.","spans":[]},{"type":"paragraph","text":"Since creating and maintaining an app image is repetitive and has a lot of detailed steps that easily break when doing them manually, we chose to handle the process using Packer, which can automate all these processes across many different cloud providers.","spans":[{"start":171,"end":177,"type":"hyperlink","data":{"link_type":"Web","url":"https://www.packer.io"}}]},{"type":"paragraph","text":"With Packer, you provide the configuration scripts and your API key for your cloud provider, and Packer will do the rest. The result is a ready-to-use image for each cloud provider. Most importantly, everything is automatic, which means it's reproducible and can be part of continuous integration. Let's dig a little bit deeper to see how to do it in detail.","spans":[]},{"type":"paragraph","text":"There are quite a few ways to install Packer as you can see in the official documentation. The simplest way is to use the precompiled binary.  ","spans":[{"start":67,"end":89,"type":"hyperlink","data":{"link_type":"Web","url":"https://www.packer.io/intro/getting-started/install.html"}},{"start":122,"end":140,"type":"hyperlink","data":{"link_type":"Web","url":"https://www.packer.io/downloads.html"}}]},{"type":"heading3","text":"Obtaining a DigitalOcean API Token","spans":[]},{"type":"paragraph","text":"To use Packer with DigitalOcean, we first need to obtain (or generate) a DigitalOcean API Token. This can be done on the DigitalOcean website:","spans":[{"start":121,"end":141,"type":"hyperlink","data":{"link_type":"Web","url":"https://cloud.digitalocean.com/account/api/tokens"}}]},{"type":"image","url":"https://images.prismic.io/www-static/078cf34e-59bd-4cee-8ae7-e85313ff016c_api_token.png?auto=compress,format","alt":null,"copyright":null,"dimensions":{"width":1600,"height":526}},{"type":"paragraph","text":"Note that both read and write scope are needed for the API token.","spans":[]},{"type":"heading3","text":"DigitalOcean Packer Builder","spans":[]},{"type":"paragraph","text":"A builder in Packer will create an image on a certain platform from some configuration, and DigitalOcean is one of the supported builders. A basic Packer example which creates a plain Ubuntu image on DigitalOcean is as simple as the following Packer template:  ","spans":[]},{"type":"preformatted","text":"```[php]{`","spans":[]},{"type":"paragraph","text":"    {","spans":[]},{"type":"paragraph","text":"      \"builders\": [","spans":[]},{"type":"paragraph","text":"        {","spans":[]},{"type":"paragraph","text":"          \"type\": \"digitalocean\",","spans":[]},{"type":"paragraph","text":"          \"api_token\": \"Your DigitalOcean API Token\",","spans":[]},{"type":"paragraph","text":"          \"image\": \"ubuntu-18-04-x64\",","spans":[]},{"type":"paragraph","text":"          \"region\": \"nyc3\",","spans":[]},{"type":"paragraph","text":"          \"size\": \"s-1vcpu-1gb\",","spans":[]},{"type":"paragraph","text":"          \"ssh_username\": \"root\"","spans":[]},{"type":"paragraph","text":"        }","spans":[]},{"type":"paragraph","text":"      ]","spans":[]},{"type":"paragraph","text":"    }","spans":[]},{"type":"preformatted","text":"`}```","spans":[]},{"type":"paragraph","text":"If you save the above Packer template as ```[php]{`packer.json`}```, include your DigitalOcean API token and run ```[php]{`packer build packer.json`}```, you will get a working Ubuntu image in your DigitalOcean account in a couple minutes!","spans":[]},{"type":"paragraph","text":"If we look at the template above, the ```[php]{`image`}``` key specifies the basic OS image to use; the ```[php]{`region`}``` and ```[php]{`size`}``` are used for the temporary droplet packer creates (and will destroy automatically); and, the ```[php]{`ssh_username`}``` key specifies which user Packer will use to ssh into the machine.","spans":[]},{"type":"paragraph","text":"You probably noticed that builders in the Packer template are an array, so if we want to build the same image on multiple cloud providers, we just need to add builders into the array, and all of them will be built in parallel.","spans":[]},{"type":"paragraph","text":"Packer supports quite a lot of builders. More information about Packer builder can be found here.","spans":[{"start":92,"end":96,"type":"hyperlink","data":{"link_type":"Web","url":"https://www.packer.io/docs/builders/index.html"}}]},{"type":"heading3","text":"Provisioning the Image","spans":[]},{"type":"paragraph","text":"The above basic example only gives you a standard Ubuntu image, which is probably not what you want. To build a custom image, we need provisioners that customize an image by installing and configuring software before taking the Snapshot and turning it into an image. Let's add provisioners to the Packer template in the previous step:","spans":[]},{"type":"preformatted","text":"```[php]{`","spans":[]},{"type":"paragraph","text":"    {","spans":[]},{"type":"paragraph","text":"      \"builders\": [","spans":[]},{"type":"paragraph","text":"        {","spans":[]},{"type":"paragraph","text":"          \"type\": \"digitalocean\",","spans":[]},{"type":"paragraph","text":"          \"api_token\": \"Your DigitalOcean API Token\",","spans":[]},{"type":"paragraph","text":"          \"image\": \"ubuntu-18-04-x64\",","spans":[]},{"type":"paragraph","text":"          \"region\": \"nyc3\",","spans":[]},{"type":"paragraph","text":"          \"size\": \"s-1vcpu-1gb\",","spans":[]},{"type":"paragraph","text":"          \"ssh_username\": \"root\"","spans":[]},{"type":"paragraph","text":"        }","spans":[]},{"type":"paragraph","text":"      ],","spans":[]},{"type":"paragraph","text":"      \"provisioners\": [","spans":[]},{"type":"paragraph","text":"        {","spans":[]},{"type":"paragraph","text":"          \"type\": \"shell\",","spans":[]},{"type":"paragraph","text":"          \"inline\": [","spans":[]},{"type":"paragraph","text":"            \"sleep 30\",","spans":[]},{"type":"paragraph","text":"            \"sudo apt-get update\",","spans":[]},{"type":"paragraph","text":"            \"sudo apt-get install -y supervisor\"","spans":[]},{"type":"paragraph","text":"          ]","spans":[]},{"type":"paragraph","text":"        },","spans":[]},{"type":"paragraph","text":"        {","spans":[]},{"type":"paragraph","text":"          \"type\": \"shell\",","spans":[]},{"type":"paragraph","text":"          \"script\": \"./scripts.sh\"","spans":[]},{"type":"paragraph","text":"        },","spans":[]},{"type":"paragraph","text":"        {","spans":[]},{"type":"paragraph","text":"          \"type\": \"file\",","spans":[]},{"type":"paragraph","text":"          \"source\": \"./nkn.conf\",","spans":[]},{"type":"paragraph","text":"          \"destination\": \"/tmp/\"","spans":[]},{"type":"paragraph","text":"        },","spans":[]},{"type":"paragraph","text":"        {","spans":[]},{"type":"paragraph","text":"          \"type\": \"shell\",","spans":[]},{"type":"paragraph","text":"          \"inline\": [","spans":[]},{"type":"paragraph","text":"            \"sudo mv /tmp/nkn.conf /etc/supervisor/conf.d/\"","spans":[]},{"type":"paragraph","text":"          ]","spans":[]},{"type":"paragraph","text":"        }","spans":[]},{"type":"paragraph","text":"      ]","spans":[]},{"type":"paragraph","text":"    }","spans":[]},{"type":"preformatted","text":"`}```","spans":[]},{"type":"paragraph","text":"We added quite a few provisioners to the Packer template, and they will be executed sequentially. Let's look at them one by one.","spans":[]},{"type":"paragraph","text":"The first provisioner is an inline shell type, which contains a few commands to run on the droplet before creating the image. Note that the first command ```[php]{`sleep 30`}``` makes sure the system is properly initialized before executing the rest of the command.","spans":[]},{"type":"paragraph","text":"The second provisioner is a shell script type. It's very similar to the first one but specifies a shell script file to run on the droplet.","spans":[]},{"type":"paragraph","text":"The third provisioner uploads a local file to the droplet, and the fourth provisioner copies the uploaded file into the designated location. One can also upload it directly to the target location which works properly on DigitalOcean but not necessarily on other cloud platforms if ```[php]{`ssh_username`}``` does not have the required permissions.","spans":[]},{"type":"paragraph","text":"You can find more information about supported provisioners in the official documentation.","spans":[{"start":66,"end":88,"type":"hyperlink","data":{"link_type":"Web","url":"https://www.packer.io/docs/provisioners/index.html"}}]},{"type":"heading3","text":"Passing Variables to the Packer Template","spans":[]},{"type":"paragraph","text":"Currently, the Packer template contains your DigitalOcean API token in plain text, which is a very dangerous thing to do if you share it with other people. A much better way is to pass the API token as a variable to the template. Do this by modifying the Packer template (“provisioners” section does not need to be changed):","spans":[]},{"type":"preformatted","text":"```[php]{`","spans":[]},{"type":"paragraph","text":"    {","spans":[]},{"type":"paragraph","text":"      \"variables\": {","spans":[]},{"type":"paragraph","text":"        \"do_api_token\": \"\"","spans":[]},{"type":"paragraph","text":"      },","spans":[]},{"type":"paragraph","text":"      \"builders\": [","spans":[]},{"type":"paragraph","text":"        {","spans":[]},{"type":"paragraph","text":"          \"type\": \"digitalocean\",","spans":[]},{"type":"paragraph","text":"          \"api_token\": \"{{user `do_api_token`}}\",","spans":[]},{"type":"paragraph","text":"          \"image\": \"ubuntu-18-04-x64\",","spans":[]},{"type":"paragraph","text":"          \"region\": \"nyc3\",","spans":[]},{"type":"paragraph","text":"          \"size\": \"s-1vcpu-1gb\",","spans":[]},{"type":"paragraph","text":"          \"ssh_username\": \"root\"","spans":[]},{"type":"paragraph","text":"        }","spans":[]},{"type":"paragraph","text":"      ],","spans":[]},{"type":"paragraph","text":"      \"provisioners\": [...]","spans":[]},{"type":"paragraph","text":"    }","spans":[]},{"type":"preformatted","text":"`}```","spans":[]},{"type":"paragraph","text":"To build the image, use the command ```[php]{`packer build -var 'do_api_token=XXX' packer.json`}``` where XXX should be replaced by your DigitalOcean API token. Using the variable is not only safer, but also makes it possible to create the same image in different DigitalOcean accounts with the same template.","spans":[]},{"type":"heading3","text":"A Few Final Tips","spans":[]},{"type":"paragraph","text":"Here are some tips based on our experience in creating a multi-platform images using Packer:","spans":[]},{"type":"o-list-item","text":"Updating an image is typically more complicated than updating the code, so it's better to create your image such that it will fetch the latest version of your software when the Droplet is initiated such that the image itself does not need to be updated every time your software updates.","spans":[]},{"type":"o-list-item","text":"For the same reason as above, it's useful to have some sort of auto-update mechanism so that existing one-click users will not need to update or even re-create the Droplet manually.","spans":[]},{"type":"o-list-item","text":"All Droplets created from the same image are nearly identical on DigitalOcean, so if you have any sort of \"account\" in your software, the credentials may need to be generated randomly and saved to disk so users can access it. In our case we also set the credential file permission so that only a certain user (plus superusers) can view the content of the file.","spans":[]},{"type":"o-list-item","text":"Do not assume ```[php]{`ssh_username`}``` is a superuser like root. Although we use root user on DigitalOcean, other platforms may have different user settings, and provisioners may not work properly due to lack of permissions.","spans":[]},{"type":"heading3","text":"Complete Code","spans":[]},{"type":"paragraph","text":"The complete config we used to create 1-click image on DigitalOcean and other platforms can be found at GitHub.","spans":[{"start":104,"end":110,"type":"hyperlink","data":{"link_type":"Web","url":"https://github.com/nknorg/nkn-cloud-image"}}]},{"type":"paragraph","text":"Yilun Zhang is co-founder and CTO of NKN.org.","spans":[{"start":0,"end":45,"type":"em"}]}],"blog_post_date":"2019-03-07","tags":[{"tag1":{"tag":"Marketplace","_linkType":"Link.document","_meta":{"uid":"marketplace"}}}],"_meta":{"uid":"using-packer-to-create-a-1-click-nkn-image-on-digitalocean"}}},{"node":{"author":{"_linkType":"Link.document","author_name":"Paul Jarvis","author_image":{"dimensions":{"width":1861,"height":1861},"alt":"Paul Jarvis","copyright":null,"url":"https://images.prismic.io/www-static/71f5eb2e289151778a7c7e71918b8b9f3a26d12b_pauljarvis01-1.jpg?auto=compress,format"},"_meta":{"uid":"paul_jarvis"}},"blog_header_image":{"dimensions":{"width":800,"height":600},"alt":"laptop illustration","copyright":null,"url":"https://images.prismic.io/www-static/2406e46c6320ad83e8d9f275de03872e03b759ff_jarvis-company-of-one.png?auto=compress,format"},"blog_headline":[{"type":"heading1","text":"Why You Need These Starter Apps for Any Indie Business","spans":[]}],"blog_post_content":[{"type":"paragraph","text":"Adam Wathan is a popular full-stack developer and entrepreneur who writes code and books for a living. He can easily whip up his own payment processing system with Stripe’s API—something he did for his first product, a video course about Laravel. But for his most recent product, Refactoring UI, he instead chose to use the ecommerce platform Gumroad.","spans":[{"start":0,"end":11,"type":"hyperlink","data":{"link_type":"Web","url":"https://adamwathan.me/"}},{"start":164,"end":176,"type":"hyperlink","data":{"link_type":"Web","url":"https://stripe.com/docs/api"}}]},{"type":"paragraph","text":"Why? Because, by using existing software, he was able to focus on the product and its marketing instead of reinventing the wheel. Gumroad had the features Adam needed, including support for PayPal payments in addition to credit card processing. And, as an added bonus, it acts as the \"merchant of record,\" which means they sell his products and collect sales tax, freeing him of those burdens too.","spans":[]},{"type":"paragraph","text":"The faster you can get your own software out the door to paying customers, the faster you’ll become profitable and can start building traction. Existing software is either free or cheap until you need to scale it—and if you’re scaling it, it means you’re generating a decent amount of revenue! For Adam, giving Refactoring UI as much attention as possible paid off, generating over a million dollars from it in the first two months.","spans":[]},{"type":"paragraph","text":"When starting a small or independent business, especially one that’s based online and selling software, it’s important to have the right digital tools in your toolbox. Most of my business runs on existing platforms or other software—from using Laravel Spark as the scaffolding to handle subscription billing and password resets, to using Mailchimp to send newsletters for marketing.","spans":[{"start":244,"end":257,"type":"hyperlink","data":{"link_type":"Web","url":"https://spark.laravel.com/"}},{"start":338,"end":347,"type":"hyperlink","data":{"link_type":"Web","url":"https://mailchimp.com/"}}]},{"type":"paragraph","text":"I release at least one product a year—from books to software to online courses—and while they’re quite different from one another, I mostly use all the same basic online tools to get them launched, promoted, and most importantly, made so people can buy them from me.","spans":[]},{"type":"paragraph","text":"Here’s the software I’ve currently got in my own “software stack” for starting a small digital product business:","spans":[]},{"type":"list-item","text":"Droplet from DigitalOcean, 1-Click application: Ghost.","spans":[{"start":48,"end":53,"type":"hyperlink","data":{"link_type":"Web","url":"https://marketplace.digitalocean.com/apps/ghost?action=deploy&amp;refcode=a36c0e8abd93"}}]},{"type":"list-item","text":"This setup gives you a sharp looking and functional website in seconds, when more than just a single-page site is needed. [$5/month]","spans":[]},{"type":"list-item","text":"Droplet from DigitalOcean, 1-Click application: Fathom Analytics.","spans":[{"start":48,"end":64,"type":"hyperlink","data":{"link_type":"Web","url":"https://marketplace.digitalocean.com/apps/fathom-analytics?action=deploy&amp;refcode=a36c0e8abd93"}}]},{"type":"list-item","text":"Fathom is the most private and simple way to collect website visitor analytics from your marketing site or landing page. (And I co-built it!) [$5/month]","spans":[]},{"type":"list-item","text":"Droplet from DigitalOcean, 1-Click application: Discourse.","spans":[{"start":48,"end":57,"type":"hyperlink","data":{"link_type":"Web","url":"https://marketplace.digitalocean.com/apps/discourse?action=deploy&amp;refcode=a36c0e8abd93"}}]},{"type":"list-item","text":"If you want to start a community along with your digital product, then Discourse is the easiest way to start and maintain a message board for your customers, your audience, or even as a way for customers to help each other (this is how Ghost partially uses theirs). [$5/month]","spans":[]},{"type":"list-item","text":"Landing page site from Carrd.","spans":[{"start":23,"end":28,"type":"hyperlink","data":{"link_type":"Web","url":"https://carrd.co/"}}]},{"type":"list-item","text":"It’s a quick and easy way to put up a single page website and collect emails/gauge interest in your idea. [$9/year]","spans":[]},{"type":"list-item","text":"Mailing list from Mailchimp.","spans":[{"start":18,"end":27,"type":"hyperlink","data":{"link_type":"Web","url":"https://mailchimp.com/"}}]},{"type":"list-item","text":"Newsletters are a fantastic way to see how many people are interested in your product, and then to keep them posted on your progress. Mailing lists also have the highest return on investment (a 4400% return, meaning $44 for every $1 spent). [Free up to 2,000 subscribers]","spans":[{"start":194,"end":238,"type":"hyperlink","data":{"link_type":"Web","url":"https://www.campaignmonitor.com/company/annual-report/2016/"}}]},{"type":"list-item","text":"Selling digital products via Gumroad.","spans":[{"start":29,"end":36,"type":"hyperlink","data":{"link_type":"Web","url":"https://gumroad.com"}}]},{"type":"list-item","text":"If you’re selling downloadables like ebooks, pre-orders or membership subscriptions, Gumroad saves you the hassle of everything “payment”. [3.5% + 30¢ per charge]","spans":[]},{"type":"list-item","text":"Selling SaaS subscriptions from Stripe.","spans":[{"start":32,"end":38,"type":"hyperlink","data":{"link_type":"Web","url":"https://stripe.com/"}}]},{"type":"list-item","text":"If you’re selling more custom solutions, then just using Stripe and a bit of custom code makes it quick and easy to do subscriptions. [2.9% + 30¢ per charge]","spans":[]},{"type":"list-item","text":"Creating a plan and roadmap using Trello.","spans":[{"start":34,"end":40,"type":"hyperlink","data":{"link_type":"Web","url":"https://trello.com"}}]},{"type":"list-item","text":"This app helps you keep track of what tasks you need to do and have done, and it also enables you to share a public roadmap of your progress with your customers or audience. [Free]","spans":[]},{"type":"list-item","text":"Internal communications from Twist.","spans":[{"start":29,"end":34,"type":"hyperlink","data":{"link_type":"Web","url":"https://twist.com"}}]},{"type":"list-item","text":"If your team is more than just yourself, Twist is a great way to stay in touch with them without being overwhelmed by group messaging platforms. [$6/user/month]","spans":[]},{"type":"list-item","text":"Using Cloudflare to register domains and handle DNS security/performance.","spans":[{"start":6,"end":16,"type":"hyperlink","data":{"link_type":"Web","url":"https://www.cloudflare.com/"}}]},{"type":"list-item","text":"Using Cloudflare to manage your DNS means better defence against online threats and low-priced domain registration (they sell this at cost for TLD extensions they support). [Free]","spans":[]},{"type":"list-item","text":"Scaffolding for things you don’t want to code from Laravel Spark.","spans":[]},{"type":"list-item","text":"If you use Laravel, the Spark lets you forget the boilerplate (subscription billing, authentication, invoices, etc)  and focus on your app. [$99/site]","spans":[]},{"type":"paragraph","text":"With these 11 pieces of software you’ll be able to focus almost entirely on building your unique solutions, and not a lot of time building things you don’t have to. Building a digital product business can be difficult enough – any way you can make it easier (like using the software in this list), will help get you to profitability and beyond much faster.","spans":[]},{"type":"paragraph","text":"Paul Jarvis is the author of Company of One and cofounder of Fathom Analytics.","spans":[{"start":0,"end":78,"type":"em"},{"start":29,"end":43,"type":"hyperlink","data":{"link_type":"Web","url":"https://ofone.co/"}},{"start":61,"end":77,"type":"hyperlink","data":{"link_type":"Web","url":"https://usefathom.com"}}]},{"type":"paragraph","text":"Want more Paul? Join us March 19, 2019, from two P.M. to three P.M. EST, for an AMA (Ask Me Anything) as he discusses his latest book, Company of One, exploring why a bigger digital business isn’t always better, and shares his knowledge with the DigitalOcean community. RSVP today.","spans":[{"start":0,"end":281,"type":"strong"},{"start":270,"end":280,"type":"hyperlink","data":{"link_type":"Web","url":"https://www.eventbrite.com/e/digitalocean-presents-virtual-ask-me-anything-with-paul-jarvis-tickets-57132998336"}}]}],"blog_post_date":"2019-03-06","tags":[{"tag1":{"tag":"Marketplace","_linkType":"Link.document","_meta":{"uid":"marketplace"}}}],"_meta":{"uid":"starter-apps-for-any-indie-business"}}},{"node":{"author":{"_linkType":"Link.document","author_name":"Nick Wade","author_image":{"dimensions":{"width":556,"height":555},"alt":"Nick Wade","copyright":null,"url":"https://images.prismic.io/www-static/6ad190bd983b9cfd705aca95258383c9f373aed2_nick.png?auto=compress,format"},"_meta":{"uid":"nick_wade"}},"blog_header_image":{"dimensions":{"width":1200,"height":600},"alt":"boxes with different icons in them illustration","copyright":null,"url":"https://images.prismic.io/www-static/c5b48c32f77f85fdcae47f5e028bc6d9d550f106_marketplace_blog_header.jpg?auto=compress,format"},"blog_headline":[{"type":"heading1","text":"Introducing DigitalOcean Marketplace: Our Platform for Preconfigured 1-Click Apps and Tools","spans":[]}],"blog_post_content":[{"type":"paragraph","text":"This year, we’ve kept our focus on providing developers and teams with services that remove operational burden. We know you want to concentrate on what you’re building, to deploy quickly and seamlessly to the cloud, and to scale without hassle.","spans":[]},{"type":"paragraph","text":"Today, I’m excited to announce a big step for our community: the launch of DigitalOcean Marketplace, a platform where developers can find preconfigured applications and solutions to get up and running even more quickly.","spans":[{"start":75,"end":99,"type":"hyperlink","data":{"link_type":"Web","url":"https://marketplace.digitalocean.com/"}}]},{"type":"paragraph","text":"DigitalOcean Marketplace is designed with simplicity at its heart. We work closely with partners to deliver a truly seamless experience for users, creating the ability for developers to deploy fully tested app environments with the click of a button.","spans":[]},{"type":"heading3","text":"DigitalOcean Marketplace Makes Discovering & Integrating Apps Simple","spans":[]},{"type":"paragraph","text":"Modern app development often requires a little help from third parties. An entire ecosystem of software tools – from application frameworks to blogs and business apps – has sprung up to support developers and businesses.","spans":[]},{"type":"paragraph","text":"However, finding, installing, and maintaining compatible software can be overwhelming. Researching latest versions, configuring tools and libraries, and testing for compatibility are burdens that most developers and businesses would rather avoid.","spans":[]},{"type":"paragraph","text":"DigitalOcean Marketplace removes the pain of “dependency hell” by bringing together our user community and a network of trusted partners whose apps and tools we have carefully vetted for seamless integration and deployment. Whether you need a forum platform or an analytics package, you can deploy any app or tool on Marketplace with literally one click.","spans":[]},{"type":"heading3","text":"Marketplace Partners Provide the Services You Need","spans":[]},{"type":"paragraph","text":"We’re delighted to be launching with a handpicked set of 1-Click Apps built by technology partners who provide tools and services that have been in high demand from our developer community.","spans":[]},{"type":"paragraph","text":"","spans":[{"start":0,"end":0,"type":"hyperlink","data":{"link_type":"Web","url":"https://marketplace.digitalocean.com/apps/wordpress"}}]},{"type":"image","url":"https://images.prismic.io/www-static/967694ee2a9117aa54eeeca07462b3d4399361ec_wordpress20-20digitalocean20marketplace202019-03-012022-48-04.png?auto=compress,format","alt":"drawing","copyright":null,"dimensions":{"width":586,"height":668}},{"type":"paragraph","text":"WordPress powers a third of the web, helping millions of teams from individual bloggers to some of the biggest brands in the world create rich websites and apps online. Whatever you want to create, sell, or share, the WordPress 1-Click is there to help do it quickly.","spans":[{"start":0,"end":9,"type":"hyperlink","data":{"link_type":"Web","url":"https://marketplace.digitalocean.com/apps/wordpress"}},{"start":0,"end":267,"type":"em"}]},{"type":"paragraph","text":"","spans":[{"start":0,"end":0,"type":"hyperlink","data":{"link_type":"Web","url":"https://marketplace.digitalocean.com/apps/grafana"}}]},{"type":"image","url":"https://images.prismic.io/www-static/33b617b77072f7b43a17f07757b84770347a98b7_grafana20-20digitalocean20marketplace202019-03-012022-49-12.png?auto=compress,format","alt":"drawing","copyright":null,"dimensions":{"width":590,"height":670}},{"type":"paragraph","text":"Grafana is a data visualization and monitoring tool that integrates with complex data from sources like Prometheus, InfluxDB, Graphite, and ElasticSearch. Grafana lets you create alerts, notifications, and ad hoc filters for your data while also making collaboration with your teammates easier through built-in sharing features.","spans":[{"start":0,"end":7,"type":"hyperlink","data":{"link_type":"Web","url":"https://marketplace.digitalocean.com/apps/grafana"}},{"start":0,"end":328,"type":"em"}]},{"type":"paragraph","text":"","spans":[{"start":0,"end":0,"type":"hyperlink","data":{"link_type":"Web","url":"https://marketplace.digitalocean.com/apps/openfaas"}}]},{"type":"image","url":"https://images.prismic.io/www-static/a0a80401bfb19739cf9cf3726239572b7b9e0075_openfaas.png?auto=compress,format","alt":"drawing","copyright":null,"dimensions":{"width":594,"height":672}},{"type":"paragraph","text":"With OpenFaaS® you can package anything as a serverless function - from Node.js to Golang to CSharp, even binaries like ffmpeg or ImageMagick. Try OpenFaaS and the world of functions in 60 seconds or less with this 1-Click setup.","spans":[{"start":0,"end":229,"type":"em"},{"start":5,"end":13,"type":"hyperlink","data":{"link_type":"Web","url":"https://marketplace.digitalocean.com/apps/openfaas"}}]},{"type":"paragraph","text":"","spans":[{"start":0,"end":0,"type":"hyperlink","data":{"link_type":"Web","url":"https://marketplace.digitalocean.com/apps/influxdb-tick-stack"}}]},{"type":"image","url":"https://images.prismic.io/www-static/30b7584519a4b4509994ad432e32c68990e713b7_influxdb20-20digitalocean20marketplace202019-03-012022-49-59.png?auto=compress,format","alt":"drawing","copyright":null,"dimensions":{"width":590,"height":670}},{"type":"paragraph","text":"The InfluxData Platform is the leading modern time series platform designed from the ground up for metrics and events. The open source TICK Stack 1-Click includes everything needed to quickly make beautiful dashboards, observe Kubernetes clusters, store syslog messages, and even monitor your smart home.","spans":[{"start":0,"end":304,"type":"em"},{"start":4,"end":23,"type":"hyperlink","data":{"link_type":"Web","url":"https://marketplace.digitalocean.com/apps/influxdb-tick-stack"}}]},{"type":"paragraph","text":"","spans":[{"start":0,"end":0,"type":"hyperlink","data":{"link_type":"Web","url":"https://marketplace.digitalocean.com/apps/hasura"}}]},{"type":"image","url":"https://images.prismic.io/www-static/eef76032b9688ddb83e64c0a942cd4ea9b8579e3_hasura20graphql20-20digitalocean20marketplace202019-03-012022-51-03.png?auto=compress,format","alt":"drawing","copyright":null,"dimensions":{"width":590,"height":668}},{"type":"paragraph","text":"Hasura GraphQL Engine lets you make powerful queries with built-in filtering, pagination, pattern search, bulk insert, update, delete mutations, and subscriptions. This 1-Click setup also includes an empty Postgres database and automatic HTTPS from Let’s Encrypt using Caddy web server.","spans":[{"start":0,"end":21,"type":"hyperlink","data":{"link_type":"Web","url":"https://marketplace.digitalocean.com/apps/hasura"}},{"start":0,"end":286,"type":"em"}]},{"type":"paragraph","text":"As you can see, we have focused on the tools most useful to innovators: founders, entrepreneurs, and early-stage business leaders. Over the next few months, we will be thoughtfully adding more 1-Click Apps and categories based on feedback from the DigitalOcean community, and we plan to integrate 1-Click Apps with DigitalOcean Kubernetes.","spans":[]},{"type":"heading3","text":"The Marketplace Ecosystem is Continually Expanding","spans":[]},{"type":"paragraph","text":"Just like our community is growing by the day, so too is the list of Marketplace partners providing top-notch apps and services.","spans":[]},{"type":"paragraph","text":"We’ve been delighted with the enthusiasm and caliber of our partners – they’ve been a delight to work with.","spans":[]},{"type":"paragraph","text":"If you or your team has built something delightful, and you’d love to get it in front of a discerning developer community, then Marketplace is open for you. You’ll find everything you need to know about joining DigitalOcean Marketplace and unleashing your product to the world at our Marketplace Vendor page.","spans":[{"start":284,"end":307,"type":"hyperlink","data":{"link_type":"Web","url":"https://marketplace.digitalocean.com/vendors"}}]},{"type":"heading3","text":"Dive in!","spans":[]},{"type":"paragraph","text":"I can’t wait for you to try out Marketplace and hear your thoughts. We’ll be rolling out continuous improvements to this platform over the next few weeks, including ongoing upgrades to the Control Panel, API, and CLI, as well as supporting features such as Tags, Teams, and Projects. As always, we will be guided by your feedback and ideas – so I’d love to hear from you in the comments below!","spans":[{"start":24,"end":43,"type":"hyperlink","data":{"link_type":"Web","url":"https://marketplace.digitalocean.com/"}}]},{"type":"paragraph","text":"Happy Coding,","spans":[]},{"type":"paragraph","text":"Nick Wade","spans":[]},{"type":"paragraph","text":"Head of Ecosystem & Marketplace","spans":[]}],"blog_post_date":"2019-03-05","tags":[{"tag1":{"tag":"Product Updates","_linkType":"Link.document","_meta":{"uid":"product-updates"}}},{"tag1":{"tag":"Marketplace","_linkType":"Link.document","_meta":{"uid":"marketplace"}}}],"_meta":{"uid":"introducing-digitalocean-marketplace"}}}]}}}