With Early Release ebooks, you get books in their earliest form—the author’s raw and unedited content as they write—so you can take advantage of these technologies long before the official release of these titles.
This will be the 11th chapter of the final book. The GitHub repo is available at https://github.com/hjwp/book-example.
If you have comments about how we might improve the content and/or examples in this book, or if you notice missing material within this chapter, please reach out to the author at [email protected].
This chapter is all about getting ready for our deployment. We’re going to spin up an actual server, make it accessible on the Internet with a real domain name, and setup the authentication and credentials we need to be able to control it remotely with SSH and Ansible.
As part of my work on the third edition of the book, I’m making big changes to the deployment chapters. This chapter is still a little "bare bones" and could do with a bit more explanatory content and guidance, but the core steps are all there, so I hope you’ll be able to follow along.
So as always I really, really need feedback. So please hit me up at [email protected], or via GitHub Issues and Pull Requests.
I hope you enjoy the new version!
We’re going to need a couple of domain names at this point in the book—they can both be subdomains of a single domain. I’m going to use superlists.ottg.co.uk and staging.ottg.co.uk. If you don’t already own a domain, this is the time to register one! Again, this is something I really want you to actually do. If you’ve never registered a domain before, just pick any old registrar and buy a cheap one—it should only cost you $5 or so, and you can even find free ones. I promise seeing your site on a "real" website will be a thrill.
We can separate out "deployment" into two tasks:
-
Provisioning a new server to be able to host the code
-
Deploying a new version of the code to an existing server
Infrastructure-as-code tools can let you automate both of these, but the provisioning parts tend to be quite vendor-specific, so for the purposes of this book, we can live with manual provisioning.
Note
|
I should probably stress once more that deployment is something that varies a lot, and that as a result there are few universal best practices for how to do it. So, rather than trying to remember the specifics of what I’m doing here, you should be trying to understand the rationale, so that you can apply the same kind of thinking in the specific future circumstances you encounter. |
There are loads of different solutions out there these days, but they broadly fall into two camps:
-
Running your own (probably virtual) server, aka VPS (Virtual Private Server)
-
Using a Platform-As-A-Service (PaaS) offering like Heroku or my old employers, PythonAnywhere.
With a PaaS, you don’t get your own server, instead you’re renting a "service" at a higher level of abstraction. Particularly for small sites, a PaaS offers a lot of advantages over running your own server, and I would definitely recommend looking into them. We’re not going to use a PaaS in this book however, for several reasons. The main reason is that I want to avoid endorsing specific commercial providers. Secondly, all the PaaS offerings are quite different, and the procedures to deploy to each vary a lot—learning about one doesn’t necessarily tell you about the others. Any one of them might radically change their process or business model by the time you get to read this book.
Instead, we’ll learn just a tiny bit of good old-fashioned server admin, including SSH and manual debugging. They’re unlikely to ever go away, and knowing a bit about them will get you some respect from all the grizzled dinosaurs out there.
I’m not going to dictate how you spin up a server—whether you choose Amazon AWS, Rackspace, Digital Ocean, your own server in a data centre, or a Raspberry Pi in a cupboard under the stairs, any solution should be fine, as long as:
-
Your server is running Ubuntu 22.04 (aka "Jammy/LTS").
-
You have root access to it.
-
It’s on the public internet (ie, it has a public IP address).
-
You can SSH into it.
I’m recommending Ubuntu as a distro:footnote[ Linux as an operating system comes in lots of different flavours, called "distros" or "distributions". The differences between them and their relative pros and cons are, like any seemingly minor detail, of tremendous interest to the right kind of nerd. We don’t need to care about them for this book. As I say, Ubuntu is fine.] because it’s popular and I’m used to it. If you know what you’re doing, you can probably get away with using something else, but I won’t be able to help you as much if you get stuck.
-
TODO: explictly recommend passwordless / public key auth.
I appreciate that, if you’ve never started a Linux server before and you have absolutely no idea where to start, this is a big ask, especially when I’m refusing to "dictate" exactly how to do it.
With that in mind, I wrote a very brief guide on GitHub.
I didn’t want to include it in the book itself because, inevitably, I do end up specifying a specific commercial provider in there.
Note
|
Some people get to this chapter, and are tempted to skip the domain bit, and the "getting a real server" bit, and just use a VM on their own PC. Don’t do this. It’s not the same, and you’ll have more difficulty following the instructions, which are complicated enough as it is. If you’re worried about cost, have a look at the guide I wrote for free options. |
We don’t want to be messing about with IP addresses all the time, so we should point our staging and live domains to the server. At my registrar, the control screens looked a bit like Domain setup.
In the DNS system, pointing a domain at a specific IP address is called an "A-Record".[1] All registrars are slightly different, but a bit of clicking around should get you to the right screen in yours. You’ll need two A-records: one for the staging address and one for the live one. No need to worry about any other type of record.
DNS records take some time to "propagate" around the world (it’s controlled by a setting called "TTL", Time To Live), so once you’ve set up your A-record, you can check its progress on a "propagation checking" service like this one: https://www.whatsmydns.net/#A/staging.ottg.co.uk.
I’m planning to host my staging server at staging.ottg.co.uk
Infrastructure-as-code tools, also called "configuration management" tools, come in lots of shapes and sizes. Chef and Puppet were two of the original ones, and you’ll probably come across Terraform, which is particularly strong on managing cloud services like AWS.
We’re going to use Ansible, because it’s relatively popular, because it can do everything we need it to, because I’m biased that it happens to be written in Python, and because it’s probably the one I’m personally most familiar with.
Another tool could probably have worked just as well! The main thing to remember is the concept, which is that, as much as possible we want to manage our server configuration declaratively, by expressing the desired state of the server in a particular configuration syntax, rather than specifying a procedural series of steps to be followed one by one.
See Ansible and SSH.
Our objective is to use Ansible to automate the process of deploying to our server: making sure that the server has everything it needs to run our app (mostly, Docker and our container image), and then telling it to start or restart our container.
Now and again, we’ll want to "log on" to the server and have a look around manually:
for that, we’ll use the ssh
command-line on our computer,
which can let us open up an interactive console on the server.
Finally, we’ll run our functional tests against the server, once it’s running our app, to make sure it’s all working correctly.
At this point and for the rest of the book, I’m assuming that you have a nonroot user account set up, and that it has "sudo" privileges, so whenever we need to do something that requires root access, we use sudo, (or "become" in Ansible terminology); I’ll be explicit about that in the various instructions that follow.
My user is called "elspeth", but you can call yours whatever you like! Just remember to substitute it in all the places I’ve hardcoded it. See the guide I wrote (Step-by-step Instructions for Spinning up a Server) if you need tips on creating a sudo user.
Ansible uses SSH under the hood to talk to the server, so checking we can log in "manually" is a good first step:
$ ssh [email protected] elspeth@server$: echo "hello world" hello world
-
TODO: suggest passwordless login with keypairs?
Tip
|
Look out for that elspeth@server
in the command-line listings in this chapter.
It indicates commands that must be run on the server,
as opposed to commands you run on your own PC.
|
Here’s a few things to try if you can’t SSH in:
First, check network connectivity: can we even reach the server?
$ ping staging.ottg.co.uk # if that doesn't work, try the IP address $ ping 193.184.215.14 # or whatever your IP is # also see if the domain name resolves $ nslookup staging.ottg.co.uk
If the IP works and the domain name doesn’t,
and/or if the nslookup
doesn’t work,
you should go check your DNS config at your registrar.
You may just need to wait!
Try a DNS propagation checker like https://www.whatsmydns.net/#A/staging.ottg.co.uk.
Next, let’s try and debug any possible issues with authentication.
First, your hosting provider might have the option to open a console directly from within their web UI. That’s worth trying, and if there are any problems there, then you probably need to restart your server, or perhaps kill it and create a new one.
Tip
|
It’s worth double-checking your IP address at this point, in your provider’s server control panel pages. |
Next we can try debugging our SSH connection
# try the -v flag which turn on verbose/debug output $ ssh -v [email protected] OpenSSH_9.7p1, LibreSSL 3.3.6 debug1: Reading configuration data ~/.ssh/config debug1: Reading configuration data ~/.colima/ssh_config debug1: Reading configuration data /etc/ssh/ssh_config debug1: /etc/ssh/ssh_config line 21: include /etc/ssh/ssh_config.d/* matched no files debug1: /etc/ssh/ssh_config line 54: Applying options for * debug1: Authenticator provider $SSH_SK_PROVIDER did not resolve; disabling debug1: Connecting to staging.ottg.uk port 22. ssh: Could not resolve hostname staging.ottg.uk: nodename nor servname provided, or not known # oops I made a typo! it should be ottg.co.uk not ottg.uk
If that doesn’t help, try switching to the root user instead:
$ ssh -v [email protected] [...] debug1: Authentications that can continue: publickey debug1: Next authentication method: publickey debug1: get_agent_identities: bound agent to hostkey debug1: get_agent_identities: agent returned 1 keys debug1: Will attempt key: ~/.ssh/id_ed25519 ED25519 SHA256:gZLxb9zCuGVT1Dm8vB4RRnPMThe27dRzxCSYeiSzn0E agent debug1: Will attempt key: ~/.ssh/id_rsa debug1: Will attempt key: ~/.ssh/id_ecdsa debug1: Will attempt key: ~/.ssh/id_ecdsa_sk debug1: Will attempt key: ~/.ssh/id_ed25519_sk debug1: Will attempt key: ~/.ssh/id_xmss debug1: Will attempt key: ~/.ssh/id_dsa debug1: Offering public key: ~/.ssh/id_ed25519 [...] debug1: Server accepts key: ~/.ssh/id_ed25519 [...] Authenticated to staging.ottg.co.uk ([165.232.110.81]:22) using "publickey".
That one actually worked, but in the verbose output, you can watch to make sure it find the right SSH keys, for example.
Tip
|
If root works but your nonroot user doesn’t,
you may need to add your public key to
/home/yournonrootuser/.ssh/authorized_keys
|
If root doesn’t work either, you may need to add your public SSH key to your account settings page, via your provider’s web UI. That may or may not take effect immediately, you might need to delete your old server and create a new one.
Remember, that probably means a new IP address!
A serious discussion of server security is beyond the scope of this book, and I’d warn against running your own servers without learning a good bit more about it. (One reason people choose to use a PaaS to host their code is that it means a slightly fewer security issues to worry about.) If you’d like a place to start, here’s as good a place as any: https://blog.codelitt.com/my-first-10-minutes-on-a-server-primer-for-securing-ubuntu/
I can definitely recommend the eye-opening experience of installing fail2ban and watching its logfiles to see just how quickly it picks up on random drive-by attempts to brute force your SSH login. The internet is a wild place!
Assuming we can reliably SSH into the server, it’s time to install Ansible and make sure it can talk to our server as well.
Take a look at the Ansible installation guide for all the various options, but probably the simplest thing to do is to install Ansible into the virtualenv on our local machine (Ansible doesn’t need to be installed on the server):
$ pip install ansible # we also need the Docker SDK for the ansible/docker integration to work: $ pip install docker
This is the last step in making sure we’re ready, making sure Ansible can talk to our server.
At the core of ansible is what’s called a "playbook", which describes what we want to happen on our server.
Let’s create one now. It’s probably a good idea to keep it in a folder of its own:
mkdir infra
And here’s a minimal playbook whose job is just to "ping" the server, to check we can talk to it. It’s in a format called YAML (Yet Another Markup Language), which, if you’ve never come across before, you will soon develop a love-hate relationship[2] for.
- hosts: all
tasks:
- name: Ping to make sure we can talk to our server
ansible.builtin.ping:
We won’t worry too much about the syntax or how it works at the moment, let’s just use it to make sure everything works.
To invoke ansible, we use the command ansible-playbook
,
which will have been installed into your virutalenv when we did
the pip install ansible
earlier.
Here’s the full command we’ll use, with an explanation of each part:
ansible-playbook \ --user=elspeth \ (1) -i staging.ottg.co.uk, \ (2)(3) infra/deploy-playbook.yaml.yaml \ (4) -vv (5)
-
The
--user=
flag lets us specify the user to use to authenticate with the server. This should be the same user you can SSH with. -
The
-i
flag specifies what server to run against. -
Note the trailing comma after the server hostname. Without this it won’t work (it’s there because Ansible is designed to work against multiple servers at the same time).[3]
-
Next comes the path to our playbook, as a positional argument
-
Finally the
-v
or-vv
flags control how verbose the output will be. Useful for debugging!
Here’s some example output when I run it:
$ ansible-playbook --user=elspeth -i staging.ottg.co.uk, infra/deploy-playbook.yaml.yaml -vv ansible-playbook [core 2.17.5] config file = None configured module search path = ['~/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules'] ansible python module location = ...goat-book/.venv/lib/python3.13/site-packages/ansible ansible collection location = ~/.ansible/collections:/usr/share/ansible/collections executable location = ...goat-book/.venv/bin/ansible-playbook python version = 3.13.0 (main, Oct 11 2024, 22:59:05) [Clang 15.0.0 (clang-1500.3.9.4)] (...goat-book/.venv/bin/python) jinja version = 3.1.4 libyaml = True No config file found; using defaults Skipping callback 'default', as we already have a stdout callback. Skipping callback 'minimal', as we already have a stdout callback. Skipping callback 'oneline', as we already have a stdout callback. PLAYBOOK: deploy-playbook.yaml ************************************************************************************************************** 1 plays in infra/deploy-playbook.yaml PLAY [all] ********************************************************************************************************************************** TASK [Gathering Facts] ********************************************************************************************************************** task path: ...goat-book/source/chapter_11_server_prep/superlists/infra/deploy-playbook.yaml:1 [WARNING]: Platform linux on host staging.ottg.co.uk is using the discovered Python interpreter at /usr/bin/python3.10, but future installation of another Python interpreter could change the meaning of that path. See https://docs.ansible.com/ansible- core/2.17/reference_appendices/interpreter_discovery.html for more information. ok: [staging.ottg.co.uk] TASK [Ping to make sure we can talk to our server] ****************************************************************************************** task path: ...goat-book/source/chapter_11_server_prep/superlists/infra/deploy-playbook.yaml:3 ok: [staging.ottg.co.uk] => {"changed": false, "ping": "pong"} PLAY RECAP ********************************************************************************************************************************** staging.ottg.co.uk : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Looking good! In the next chapter, we’ll use Ansible to get our app up and running on our server. It’ll be a thrill I promise!
- VPS vs PaaS
-
We discussed the tradeoffs of running your own server vs opting for a PaaS. A VPS is great for learning, but you might find the lower admin overhead of a PaaS makes sense for real projects.
- Domain Name Registration and DNS
-
This tends to be something you only do once, but buying a domain name and pointing it at your server is an unavoidable part of hosting a web app. Now you know your TTLs from your A-Records!
- SSH
-
SSH is the swiss army knife of server admin. The dream is that everything is automated, but now and again you just gotta open up a shell on that box!
- Ansible
-
We’ve had the barest of teasers, but we have Ansible and we’re ready to learn how to use it.