diff options
Diffstat (limited to '_posts/2023-07-01-bringing-all-of-my-projects-together-under-one-umbrella.md')
| -rw-r--r-- | _posts/2023-07-01-bringing-all-of-my-projects-together-under-one-umbrella.md | 282 |
1 files changed, 282 insertions, 0 deletions
diff --git a/_posts/2023-07-01-bringing-all-of-my-projects-together-under-one-umbrella.md b/_posts/2023-07-01-bringing-all-of-my-projects-together-under-one-umbrella.md new file mode 100644 index 0000000..4bc45ce --- /dev/null +++ b/_posts/2023-07-01-bringing-all-of-my-projects-together-under-one-umbrella.md | |||
| @@ -0,0 +1,282 @@ | |||
| 1 | --- | ||
| 2 | title: "Bringing all of my projects together under one umbrella" | ||
| 3 | permalink: /bringing-all-of-my-projects-together-under-one-umbrella.html | ||
| 4 | date: 2023-07-01T18:49:07+02:00 | ||
| 5 | layout: post | ||
| 6 | type: post | ||
| 7 | draft: false | ||
| 8 | --- | ||
| 9 | |||
| 10 | ## What is the issue anyway? | ||
| 11 | |||
| 12 | Over the years, I have accumulated a bunch of virtual servers on my | ||
| 13 | [DigitalOcean](https://www.digitalocean.com/) account for small experimental | ||
| 14 | projects I dabble in. And this has resulted in quite a bill. I mean, I wouldn't | ||
| 15 | care if these projects were actually being used. But there were just being there | ||
| 16 | unused and wasting resources. Which makes this an unnecessary burden for me. | ||
| 17 | |||
| 18 | Most of them are just small HTML pages that have an endpoint or two to read data | ||
| 19 | from or to, and for that reason I wrote servers left and right. To be honest, | ||
| 20 | all of those things could have been done with [CGI | ||
| 21 | scripts](https://en.wikipedia.org/wiki/Common_Gateway_Interface) and that would | ||
| 22 | have been more than enough. | ||
| 23 | |||
| 24 | Recently, I decided to stop language hopping and focus on a simpler stack which | ||
| 25 | includes C, Go and Lua. And I can accomplish all the things I am interested in. | ||
| 26 | |||
| 27 | ## Finding a web server replacement | ||
| 28 | |||
| 29 | Usually I had [Nginx](https://nginx.org/en/) in front of these small web servers | ||
| 30 | and I had to manage SSL certificates and all that jazz. I am bored with these | ||
| 31 | things. I don't want to manage any of this bullshit anymore. | ||
| 32 | |||
| 33 | So the logical move forward was to find a solid alternative for this. I have | ||
| 34 | ended up on [Caddy server](https://caddyserver.com/). I've used it in the past | ||
| 35 | but kind of forgotten about it. What I really like about it is an ease of use | ||
| 36 | and a bunch of out of the box functionalities that come with it. | ||
| 37 | |||
| 38 | These are the _pitch_ points from their website: | ||
| 39 | |||
| 40 | - **Secure by Default**: Caddy is the only web server that uses HTTPS by | ||
| 41 | default. A hardened TLS stack with modern protocols preserves privacy and | ||
| 42 | exposes MITM attacks. | ||
| 43 | - **Config API**: As its primary mode of configuration, Caddy's REST API makes | ||
| 44 | it easy to automate and integrate with your apps. | ||
| 45 | - **No Dependencies**: Because Caddy is written in Go, its binaries are entirely | ||
| 46 | self-contained and run on every platform, including containers without libc. | ||
| 47 | - **Modular Stack**: Take back control over your compute edge. Caddy can be | ||
| 48 | extended with everything you need using plugins. | ||
| 49 | |||
| 50 | I had just a few requirements: | ||
| 51 | |||
| 52 | - Automatic SSL | ||
| 53 | - Static file server | ||
| 54 | - Basic authentication | ||
| 55 | - CGI script support | ||
| 56 | |||
| 57 | And the vanilla version does all of it, but CGI scripts. But that can easily be | ||
| 58 | fixed with their modular approach. You can do this on their website and build a | ||
| 59 | custom version of the server, or do it with Docker. | ||
| 60 | |||
| 61 | This is a `Dockerfile` I used to build a custom server. | ||
| 62 | |||
| 63 | ```Dockerfile | ||
| 64 | FROM caddy:builder AS builder | ||
| 65 | |||
| 66 | RUN xcaddy build \ | ||
| 67 | --with github.com/aksdb/caddy-cgi | ||
| 68 | |||
| 69 | FROM caddy:latest | ||
| 70 | RUN apk add --no-cache nano | ||
| 71 | |||
| 72 | COPY --from=builder /usr/bin/caddy /usr/bin/caddy | ||
| 73 | ``` | ||
| 74 | |||
| 75 | ## Getting rid of all the unnecessary virtual machines | ||
| 76 | |||
| 77 | The next step was to get a handle on the number of virtual servers I have all | ||
| 78 | over the place. | ||
| 79 | |||
| 80 | I decided to move all the projects and services into two main VMs: | ||
| 81 | |||
| 82 | - personal server (still Nginx) | ||
| 83 | - git server | ||
| 84 | - static file server | ||
| 85 | - personal blog | ||
| 86 | - projects server (Caddy server) | ||
| 87 | - personal experiments | ||
| 88 | - other projects | ||
| 89 | |||
| 90 | I will focus on projects' server in this post since it's more interesting. | ||
| 91 | |||
| 92 | ## Testing CGI scripts | ||
| 93 | |||
| 94 | The first thing I tested was how CGI scripts work under Caddy. This is | ||
| 95 | particularly import to me because almost all of my experiments and mini projects | ||
| 96 | need this to work. | ||
| 97 | |||
| 98 | To configure Caddy server, you must provide the server with a configuration | ||
| 99 | file. By default, it's called `Caaddyfile`. | ||
| 100 | |||
| 101 | ```caddyfile | ||
| 102 | { | ||
| 103 | order cgi before respond | ||
| 104 | } | ||
| 105 | |||
| 106 | examples.mitjafelicijan.com { | ||
| 107 | cgi /bash-test /opt/projects/examples/bash-test.sh | ||
| 108 | cgi /tcl-test /opt/projects/examples/tcl-test.tcl | ||
| 109 | cgi /lua-test /opt/projects/examples/lua-test.lua | ||
| 110 | cgi /python-test /opt/projects/examples/python-test.py | ||
| 111 | |||
| 112 | root * /opt/projects/examples | ||
| 113 | file_server | ||
| 114 | } | ||
| 115 | ``` | ||
| 116 | |||
| 117 | - The order is very important. Make sure that `order cgi before respond` is at | ||
| 118 | the top of the configuration file. | ||
| 119 | - Also, when you run with Caddy v2, make sure you provide `adapter` argument | ||
| 120 | like this `/usr/bin/caddy run --watch --environ --config /etc/caddy/Caddyfile | ||
| 121 | --adapter caddyfile`. Otherwise, Caddy will try to use a different format for | ||
| 122 | config file. | ||
| 123 | |||
| 124 | I did a small batch of tests with [Bash](https://www.gnu.org/software/bash/), | ||
| 125 | [Tcl](https://www.tcl-lang.org/), [Lua](https://www.lua.org/) and | ||
| 126 | [Python](https://www.python.org/). Here is a cheat sheet if you need it. | ||
| 127 | |||
| 128 | Let's get Bash out of the way first. | ||
| 129 | |||
| 130 | ```bash | ||
| 131 | #!/usr/bin/bash | ||
| 132 | |||
| 133 | printf "Content-type: text/plain\n\n" | ||
| 134 | |||
| 135 | printf "Hello from Bash\n\n" | ||
| 136 | printf "PATH_INFO [%s]\n" $PATH_INFO | ||
| 137 | printf "QUERY_STRING [%s]\n" $QUERY_STRING | ||
| 138 | printf "\n" | ||
| 139 | |||
| 140 | for i in {0..9..1}; do | ||
| 141 | printf "> %s\n" $i | ||
| 142 | done | ||
| 143 | |||
| 144 | exit 0 | ||
| 145 | ``` | ||
| 146 | |||
| 147 | This one is for Tcl script. | ||
| 148 | |||
| 149 | ```tcl | ||
| 150 | #!/usr/bin/tclsh | ||
| 151 | |||
| 152 | puts "Content-type: text/plain\n" | ||
| 153 | |||
| 154 | puts "Hello from Tcl\n" | ||
| 155 | puts "PATH_INFO \[$env(PATH_INFO)\]" | ||
| 156 | puts "QUERY_STRING \[$env(QUERY_STRING)\]" | ||
| 157 | puts "" | ||
| 158 | |||
| 159 | for {set i 0} {$i < 10} {incr i} { | ||
| 160 | puts "> $i" | ||
| 161 | } | ||
| 162 | ``` | ||
| 163 | |||
| 164 | And for all you Python enjoyers. | ||
| 165 | |||
| 166 | ```python | ||
| 167 | #!/usr/bin/python3 | ||
| 168 | |||
| 169 | import os | ||
| 170 | |||
| 171 | print("Content-type: text/plain\n") | ||
| 172 | |||
| 173 | print("Hello from Python\n") | ||
| 174 | print("PATH_INFO [{}]".format(os.environ['PATH_INFO'])) | ||
| 175 | print("QUERY_STRING [{}]".format(os.environ['QUERY_STRING'])) | ||
| 176 | print("") | ||
| 177 | |||
| 178 | for i in range(10): | ||
| 179 | print("> {}".format(i)) | ||
| 180 | ``` | ||
| 181 | |||
| 182 | And for the final example, Lua. | ||
| 183 | |||
| 184 | ```lua | ||
| 185 | #!/usr/bin/lua | ||
| 186 | |||
| 187 | print("Content-type: text/plain\n") | ||
| 188 | |||
| 189 | print("Hello from Lua\n") | ||
| 190 | print(string.format("PATH_INFO [%s]", os.getenv("PATH_INFO"))) | ||
| 191 | print(string.format("QUERY_STRING [%s]", os.getenv("QUERY_STRING"))) | ||
| 192 | print() | ||
| 193 | |||
| 194 | for i = 0, 9 do | ||
| 195 | print(string.format("> %d", i)) | ||
| 196 | end | ||
| 197 | ``` | ||
| 198 | |||
| 199 | ## Basic authentication | ||
| 200 | |||
| 201 | One thing was also to have an option for some sort of authentication, and | ||
| 202 | something like [Basic access | ||
| 203 | authentication](https://en.wikipedia.org/wiki/Basic_access_authentication) would | ||
| 204 | be more than enough. | ||
| 205 | |||
| 206 | Thankfully, Caddy supports this out of the box already. Below is an updated | ||
| 207 | example. | ||
| 208 | |||
| 209 | ```Caddyfile | ||
| 210 | { | ||
| 211 | order cgi before respond | ||
| 212 | } | ||
| 213 | |||
| 214 | examples.mitjafelicijan.com { | ||
| 215 | cgi /bash-test /opt/projects/examples/bash-test.sh | ||
| 216 | cgi /tcl-test /opt/projects/examples/tcl-test.tcl | ||
| 217 | cgi /lua-test /opt/projects/examples/lua-test.lua | ||
| 218 | cgi /python-test /opt/projects/examples/python-test.py | ||
| 219 | |||
| 220 | root * /opt/projects/examples | ||
| 221 | file_server | ||
| 222 | |||
| 223 | basicauth * { | ||
| 224 | bob $2a$14$/wCgaf9oMnmQa20txB76u.nI1AldGMBT/1J7fXCfgOiRShwz/JOkK | ||
| 225 | } | ||
| 226 | } | ||
| 227 | ``` | ||
| 228 | |||
| 229 | `basicauth *` matches everything under this domain/sub-domain and protects it | ||
| 230 | with Basic Authentication. | ||
| 231 | |||
| 232 | - `bob` is the username | ||
| 233 | - `hash` is the password | ||
| 234 | |||
| 235 | To generate these passwords, execute `caddy hash-password` and this will prompt | ||
| 236 | you to insert a password twice and spit out a hashed password that you can put | ||
| 237 | in your configuration file. | ||
| 238 | |||
| 239 | Restart the server and you are ready to go. | ||
| 240 | |||
| 241 | ## Making Caddy a service with systemd | ||
| 242 | |||
| 243 | After the tests were successful, I copied `caddy` to `/usr/bin/caddy` and copied | ||
| 244 | `Caddyfile` to `/etc/caddy/Caddyfile`. | ||
| 245 | |||
| 246 | Now off to the systemd. Each systemd service requires you to create a service | ||
| 247 | file. | ||
| 248 | |||
| 249 | - I created a `/etc/systemd/system/caddy.service` and put the following content | ||
| 250 | in the file. | ||
| 251 | |||
| 252 | ```systemd | ||
| 253 | [Unit] | ||
| 254 | Description=Caddy | ||
| 255 | Documentation=https://caddyserver.com/docs/ | ||
| 256 | After=network.target network-online.target | ||
| 257 | Requires=network-online.target | ||
| 258 | |||
| 259 | [Service] | ||
| 260 | Type=notify | ||
| 261 | User=root | ||
| 262 | Group=root | ||
| 263 | ExecStart=/usr/bin/caddy run --environ --config /etc/caddy/Caddyfile --adapter caddyfile | ||
| 264 | ExecReload=/usr/bin/caddy reload --config /etc/caddy/Caddyfile --force --adapter caddyfile | ||
| 265 | TimeoutStopSec=5s | ||
| 266 | LimitNOFILE=1048576 | ||
| 267 | LimitNPROC=512 | ||
| 268 | PrivateTmp=true | ||
| 269 | ProtectSystem=full | ||
| 270 | AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE | ||
| 271 | |||
| 272 | [Install] | ||
| 273 | WantedBy=multi-user.target | ||
| 274 | ``` | ||
| 275 | |||
| 276 | - You might need to reload systemd with `systemctl daemon-reload`. | ||
| 277 | - Then I enabled the service with `systemctl enable caddy.service`. | ||
| 278 | - And then I started the service with `systemctl start caddy.service`. | ||
| 279 | |||
| 280 | This was about all that I needed to do to get it running. Now I can easily add | ||
| 281 | new subdomains and domains to the main configuration file and be done with | ||
| 282 | it. No manual Let's Encrypt shenanigans needed. | ||
